From 386291672bd0a0b5d4e63326abcc2c556c226630 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Tue, 14 Nov 2023 21:36:21 -0800 Subject: [PATCH 1/5] Animated bottom sheet content opacity --- src/components/GameBottomSheet.tsx | 0 src/screens/GameScreen.tsx | 87 +++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 src/components/GameBottomSheet.tsx diff --git a/src/components/GameBottomSheet.tsx b/src/components/GameBottomSheet.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/screens/GameScreen.tsx b/src/screens/GameScreen.tsx index 3a0102c4..742c88bb 100644 --- a/src/screens/GameScreen.tsx +++ b/src/screens/GameScreen.tsx @@ -1,16 +1,24 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { View, StyleSheet, LayoutChangeEvent, Text } from 'react-native'; +import { View, StyleSheet, LayoutChangeEvent, Text, Dimensions, useWindowDimensions } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { getContrastRatio } from 'colorsheet'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { ParamListBase } from '@react-navigation/native'; -import BottomSheet, { BottomSheetScrollView } from '@gorhom/bottom-sheet'; +import BottomSheet, { BottomSheetScrollView, useBottomSheet } from '@gorhom/bottom-sheet'; import { useAppSelector } from '../../redux/hooks'; import PlayerTile from '../components/PlayerTile'; import Rounds from '../components/Rounds'; import { selectGameById } from '../../redux/GamesSlice'; import { systemBlue } from '../constants'; +import Animated, { Extrapolate, interpolate, useAnimatedStyle, useDerivedValue, useSharedValue } from 'react-native-reanimated'; + + +/** + * Height of the bottom sheet + */ +const bottomSheetHeight = 90; + interface Props { navigation: NativeStackNavigationProp; @@ -97,20 +105,68 @@ const ScoreBoardScreen: React.FunctionComponent = ({ navigation }) => { const bottomSheetRef = useRef(null); // variables - const snapPoints = useMemo(() => [73, '60%', '100%'], []); + const snapPoints = useMemo(() => [bottomSheetHeight, '60%', '100%'], []); // callbacks const handleSheetChanges = useCallback((index: number) => { console.log('handleSheetChanges', index); }, []); - const handleSnapPress = useCallback((index: number) => { - bottomSheetRef.current?.snapToIndex(index); + // State variable for the current snap point index + const [snapPointIndex, setSnapPointIndex] = useState(1); + + // Function to cycle through the snap points + const cycleSnapPoints = () => { + setSnapPointIndex((prevIndex) => { + const nextIndex = prevIndex + 1; + return nextIndex < snapPoints.length ? nextIndex : 0; + }); + }; + + // Function to snap to the next point when the button is pressed + const handleButtonPress = () => { + cycleSnapPoints(); + bottomSheetRef.current?.snapToIndex(snapPointIndex); + }; + + + const [windowHeight, setWindowHeight] = useState(0); + + // useDerivedValue(() => { + // const snapPoint0: number = typeof snapPoints[0] === 'string' + // ? parseFloat(snapPoints[0]) / 100 * windowHeight + // : windowHeight - snapPoints[0]; + + // const delta = snapPoint0 - animatedPosition.value; + // console.log("delta", delta); + + // }, [animatedPosition, windowHeight]); + + const animatedPosition = useSharedValue(0); + + const newStyles = useAnimatedStyle(() => { + const snapPoint0: number = typeof snapPoints[0] === 'string' + ? parseFloat(snapPoints[0]) / 100 * windowHeight + : windowHeight - snapPoints[0]; + + const delta = snapPoint0 - animatedPosition.value; + + const i = interpolate(delta, [0, 30], [0, 1], Extrapolate.CLAMP); + console.log("i", i); + + return { + opacity: i + }; + }); + + const onLayout = useCallback((event: LayoutChangeEvent) => { + const { height } = event.nativeEvent.layout; + setWindowHeight(height); }, []); return ( - + {playerIds.map((id, index) => ( width != null && height != null && rows != 0 && cols != 0 && @@ -135,22 +191,25 @@ const ScoreBoardScreen: React.FunctionComponent = ({ navigation }) => { onChange={handleSheetChanges} backgroundStyle={{ backgroundColor: 'rgb(30,40,50)' }} handleIndicatorStyle={{ backgroundColor: 'white' }} + animatedPosition={animatedPosition} > - + - - handleSnapPress(1)}> + handleButtonPress()}> {currentGame.title} navigation.navigate('Settings')}> Edit - - - Tap on a column to set the current round. - + + + + + Tap on a column to set the current round. + + @@ -168,7 +227,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', maxWidth: '100%', backgroundColor: '#000000', - paddingBottom: 75, + paddingBottom: bottomSheetHeight + 2, // Add 2 to account for the border }, contentContainer: { flex: 1, From 178153f0c3cc4031dcab2df612578d324412cb8d Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:54:06 -0800 Subject: [PATCH 2/5] Split out bottom sheet to own file --- src/components/GameBottomSheet.tsx | 134 +++++++++++++++++++ src/components/ScoreLog/PlayerNameColumn.tsx | 4 +- src/screens/GameScreen.tsx | 103 +------------- src/screens/ShareScreen.tsx | 15 +-- 4 files changed, 145 insertions(+), 111 deletions(-) diff --git a/src/components/GameBottomSheet.tsx b/src/components/GameBottomSheet.tsx index e69de29b..fc434a4f 100644 --- a/src/components/GameBottomSheet.tsx +++ b/src/components/GameBottomSheet.tsx @@ -0,0 +1,134 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { View, StyleSheet, Text } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { ParamListBase } from '@react-navigation/native'; +import BottomSheet, { BottomSheetScrollView } from '@gorhom/bottom-sheet'; + +import { useAppSelector } from '../../redux/hooks'; +import Rounds from '../components/Rounds'; +import { selectGameById } from '../../redux/GamesSlice'; +import { systemBlue } from '../constants'; +import Animated, { Extrapolate, interpolate, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; + +/** + * Height of the bottom sheet + */ +export const bottomSheetHeight = 90; + +interface Props { + navigation: NativeStackNavigationProp; + containerHeight: number; +} + +const GameBottomSheet: React.FunctionComponent = ({ navigation, containerHeight }) => { + const currentGameId = useAppSelector(state => state.settings.currentGameId); + if (typeof currentGameId == 'undefined') return null; + + const fullscreen = useAppSelector(state => state.settings.home_fullscreen); + const currentGame = useAppSelector(state => selectGameById(state, state.settings.currentGameId)); + + if (currentGame == undefined) return null; + + // ref + const bottomSheetRef = useRef(null); + + // variables + const snapPoints = useMemo(() => [bottomSheetHeight, '60%', '100%'], []); + + // callbacks + const handleSheetChanges = useCallback((index: number) => { + console.log('handleSheetChanges', index); + }, []); + + // State variable for the current snap point index + const [snapPointIndex, setSnapPointIndex] = useState(1); + + // Function to cycle through the snap points + const cycleSnapPoints = () => { + setSnapPointIndex((prevIndex) => { + const nextIndex = prevIndex + 1; + return nextIndex < snapPoints.length ? nextIndex : 0; + }); + }; + + // Function to snap to the next point when the button is pressed + const handleButtonPress = () => { + cycleSnapPoints(); + bottomSheetRef.current?.snapToIndex(snapPointIndex); + }; + + const animatedPosition = useSharedValue(0); + + const newStyles = useAnimatedStyle(() => { + const snapPoint0: number = typeof snapPoints[0] === 'string' + ? parseFloat(snapPoints[0]) / 100 * containerHeight + : containerHeight - snapPoints[0]; + + const delta = snapPoint0 - animatedPosition.value; + + const i = interpolate(delta, [0, 30], [0, 1], Extrapolate.CLAMP); + console.log("i", i); + + return { + opacity: i + }; + }); + + return ( + + + + + + handleButtonPress()}> + {currentGame.title} + + navigation.navigate('Settings')}> + Edit + + + + + + + Tap on a column to set the current round. + + + + + + ); +}; + +const styles = StyleSheet.create({ + sheetHeaderContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingHorizontal: 10, + }, + sheetHeader: { + color: 'white', + fontSize: 20, + paddingTop: 0, + fontWeight: 'bold' + }, + sheetHeaderButton: { + paddingHorizontal: 20, + fontSize: 20, + color: systemBlue, + }, + sheetContent: { + padding: 10, + }, +}); + +export default GameBottomSheet; diff --git a/src/components/ScoreLog/PlayerNameColumn.tsx b/src/components/ScoreLog/PlayerNameColumn.tsx index cbff3d92..16055f97 100644 --- a/src/components/ScoreLog/PlayerNameColumn.tsx +++ b/src/components/ScoreLog/PlayerNameColumn.tsx @@ -27,7 +27,7 @@ const PlayerNameColumn: React.FunctionComponent = ({ navigation, disabled ).sort((a, b) => currentGame.playerIds.indexOf(a.id) - currentGame.playerIds.indexOf(b.id)); return ( - { + { if (disabled) return; await analytics().logEvent('edit_game', { @@ -39,7 +39,7 @@ const PlayerNameColumn: React.FunctionComponent = ({ navigation, disabled   {players.map((player, index) => ( diff --git a/src/screens/GameScreen.tsx b/src/screens/GameScreen.tsx index 742c88bb..0cb0b0e5 100644 --- a/src/screens/GameScreen.tsx +++ b/src/screens/GameScreen.tsx @@ -1,23 +1,14 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { View, StyleSheet, LayoutChangeEvent, Text, Dimensions, useWindowDimensions } from 'react-native'; +import React, { useCallback, useEffect, useState } from 'react'; +import { View, StyleSheet, LayoutChangeEvent } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { getContrastRatio } from 'colorsheet'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { ParamListBase } from '@react-navigation/native'; -import BottomSheet, { BottomSheetScrollView, useBottomSheet } from '@gorhom/bottom-sheet'; import { useAppSelector } from '../../redux/hooks'; import PlayerTile from '../components/PlayerTile'; -import Rounds from '../components/Rounds'; import { selectGameById } from '../../redux/GamesSlice'; -import { systemBlue } from '../constants'; -import Animated, { Extrapolate, interpolate, useAnimatedStyle, useDerivedValue, useSharedValue } from 'react-native-reanimated'; - - -/** - * Height of the bottom sheet - */ -const bottomSheetHeight = 90; +import GameBottomSheet, { bottomSheetHeight } from '../components/GameBottomSheet'; interface Props { @@ -31,7 +22,6 @@ const ScoreBoardScreen: React.FunctionComponent = ({ navigation }) => { const palette = ["01497c", "c25858", "f5c800", "275436", "dc902c", "62516a", "755647", "925561"]; const [rows, setRows] = useState(0); const [cols, setCols] = useState(0); - const fullscreen = useAppSelector(state => state.settings.home_fullscreen); const currentGame = useAppSelector(state => selectGameById(state, state.settings.currentGameId)); const [width, setWidth] = useState(null); @@ -101,64 +91,8 @@ const ScoreBoardScreen: React.FunctionComponent = ({ navigation }) => { return dims; }; - // ref - const bottomSheetRef = useRef(null); - - // variables - const snapPoints = useMemo(() => [bottomSheetHeight, '60%', '100%'], []); - - // callbacks - const handleSheetChanges = useCallback((index: number) => { - console.log('handleSheetChanges', index); - }, []); - - // State variable for the current snap point index - const [snapPointIndex, setSnapPointIndex] = useState(1); - - // Function to cycle through the snap points - const cycleSnapPoints = () => { - setSnapPointIndex((prevIndex) => { - const nextIndex = prevIndex + 1; - return nextIndex < snapPoints.length ? nextIndex : 0; - }); - }; - - // Function to snap to the next point when the button is pressed - const handleButtonPress = () => { - cycleSnapPoints(); - bottomSheetRef.current?.snapToIndex(snapPointIndex); - }; - - const [windowHeight, setWindowHeight] = useState(0); - // useDerivedValue(() => { - // const snapPoint0: number = typeof snapPoints[0] === 'string' - // ? parseFloat(snapPoints[0]) / 100 * windowHeight - // : windowHeight - snapPoints[0]; - - // const delta = snapPoint0 - animatedPosition.value; - // console.log("delta", delta); - - // }, [animatedPosition, windowHeight]); - - const animatedPosition = useSharedValue(0); - - const newStyles = useAnimatedStyle(() => { - const snapPoint0: number = typeof snapPoints[0] === 'string' - ? parseFloat(snapPoints[0]) / 100 * windowHeight - : windowHeight - snapPoints[0]; - - const delta = snapPoint0 - animatedPosition.value; - - const i = interpolate(delta, [0, 30], [0, 1], Extrapolate.CLAMP); - console.log("i", i); - - return { - opacity: i - }; - }); - const onLayout = useCallback((event: LayoutChangeEvent) => { const { height } = event.nativeEvent.layout; setWindowHeight(height); @@ -183,36 +117,7 @@ const ScoreBoardScreen: React.FunctionComponent = ({ navigation }) => { /> ))} - - - - - - handleButtonPress()}> - {currentGame.title} - - navigation.navigate('Settings')}> - Edit - - - - - - - Tap on a column to set the current round. - - - - - + ); diff --git a/src/screens/ShareScreen.tsx b/src/screens/ShareScreen.tsx index 694b7477..3ab2d6d8 100644 --- a/src/screens/ShareScreen.tsx +++ b/src/screens/ShareScreen.tsx @@ -53,7 +53,7 @@ const ShareScreen: React.FunctionComponent = ({ navigation }) => { return ( + style={[styles.contentContainer, { height: 'auto' }]}> @@ -79,14 +79,14 @@ const ShareScreen: React.FunctionComponent = ({ navigation }) => { contentContainerStyle={{ backgroundColor: 'black', flexDirection: 'column', - padding: 10, + padding: 20, }} ref={roundsScrollViewEl}> - + {currentGame?.title} - + Created: {new Date(currentGame.dateCreated).toLocaleDateString()}   {new Date(currentGame.dateCreated).toLocaleTimeString()} @@ -105,11 +105,6 @@ const ShareScreen: React.FunctionComponent = ({ navigation }) => { ))} - - - Created with ScorePad - -