Add a fancy 'drawer' animation to the tabs selector

zio/stable
Paul Frazee 2022-11-16 17:18:16 -06:00
parent 284c635330
commit 361789975f
2 changed files with 143 additions and 159 deletions

View File

@ -7,8 +7,10 @@ import {
TouchableWithoutFeedback, TouchableWithoutFeedback,
View, View,
} from 'react-native' } from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Animated, { import Animated, {
interpolate, interpolate,
SharedValue,
useSharedValue, useSharedValue,
useAnimatedStyle, useAnimatedStyle,
withTiming, withTiming,
@ -24,12 +26,20 @@ import {LinkActionsModel} from '../../../state/models/shell-ui'
const TAB_HEIGHT = 42 const TAB_HEIGHT = 42
export const TabsSelector = observer( export const TabsSelector = observer(
({active, onClose}: {active: boolean; onClose: () => void}) => { ({
active,
tabMenuInterp,
onClose,
}: {
active: boolean
tabMenuInterp: SharedValue<number>
onClose: () => void
}) => {
const store = useStores() const store = useStores()
const insets = useSafeAreaInsets()
const [closingTabIndex, setClosingTabIndex] = useState<number | undefined>( const [closingTabIndex, setClosingTabIndex] = useState<number | undefined>(
undefined, undefined,
) )
const initInterp = useSharedValue<number>(0)
const closeInterp = useSharedValue<number>(0) const closeInterp = useSharedValue<number>(0)
const tabsRef = useRef<ScrollView>(null) const tabsRef = useRef<ScrollView>(null)
const tabRefs = useMemo( const tabRefs = useMemo(
@ -40,15 +50,10 @@ export const TabsSelector = observer(
[store.nav.tabs.length], [store.nav.tabs.length],
) )
useEffect(() => {
if (active) {
initInterp.value = withTiming(1, {duration: 150})
} else {
initInterp.value = 0
}
}, [initInterp, active])
const wrapperAnimStyle = useAnimatedStyle(() => ({ const wrapperAnimStyle = useAnimatedStyle(() => ({
bottom: interpolate(initInterp.value, [0, 1.0], [50, 75]), transform: [
{translateY: interpolate(tabMenuInterp.value, [0, 1.0], [320, 0])},
],
})) }))
// events // events
@ -118,11 +123,12 @@ export const TabsSelector = observer(
} }
return ( return (
<> <Animated.View
<TouchableWithoutFeedback onPress={onClose}> style={[
<View style={styles.bg} /> styles.wrapper,
</TouchableWithoutFeedback> {bottom: insets.bottom + 55},
<Animated.View style={[styles.wrapper, wrapperAnimStyle]}> wrapperAnimStyle,
]}>
<View onLayout={onLayout}> <View onLayout={onLayout}>
<View style={[s.p10, styles.section]}> <View style={[s.p10, styles.section]}>
<View style={styles.btns}> <View style={styles.btns}>
@ -215,27 +221,18 @@ export const TabsSelector = observer(
</View> </View>
</View> </View>
</Animated.View> </Animated.View>
</>
) )
}, },
) )
const styles = StyleSheet.create({ const styles = StyleSheet.create({
bg: {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
backgroundColor: '#000',
opacity: 0.2,
},
wrapper: { wrapper: {
position: 'absolute', position: 'absolute',
// bottom: 75,
width: '100%', width: '100%',
height: 320,
borderTopColor: colors.gray2,
borderTopWidth: 1,
backgroundColor: '#fff', backgroundColor: '#fff',
borderRadius: 8,
opacity: 1, opacity: 1,
}, },
section: { section: {
@ -244,45 +241,6 @@ const styles = StyleSheet.create({
}, },
sectionGrayBg: { sectionGrayBg: {
backgroundColor: colors.gray1, backgroundColor: colors.gray1,
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
},
fatMenuItems: {
flexDirection: 'row',
marginTop: 10,
marginBottom: 10,
},
fatMenuItem: {
width: 80,
alignItems: 'center',
marginRight: 6,
},
fatMenuItemMargin: {
marginRight: 14,
},
fatMenuItemIconWrapper: {
borderRadius: 6,
width: 60,
height: 60,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 5,
shadowColor: '#000',
shadowOpacity: 0.2,
shadowOffset: {width: 0, height: 2},
shadowRadius: 2,
},
fatMenuItemIcon: {
color: colors.white,
},
fatMenuImage: {
borderRadius: 30,
width: 60,
height: 60,
marginBottom: 5,
},
fatMenuItemLabel: {
fontSize: 13,
}, },
tabs: { tabs: {
height: 240, height: 240,

View File

@ -115,6 +115,7 @@ export const MobileShell: React.FC = observer(() => {
const scrollElRef = useRef<FlatList | undefined>() const scrollElRef = useRef<FlatList | undefined>()
const winDim = useWindowDimensions() const winDim = useWindowDimensions()
const swipeGestureInterp = useSharedValue<number>(0) const swipeGestureInterp = useSharedValue<number>(0)
const tabMenuInterp = useSharedValue<number>(0)
const screenRenderDesc = constructScreenRenderDesc(store.nav) const screenRenderDesc = constructScreenRenderDesc(store.nav)
const onPressHome = () => { const onPressHome = () => {
@ -127,7 +128,26 @@ export const MobileShell: React.FC = observer(() => {
const onPressSearch = () => store.nav.navigate('/search') const onPressSearch = () => store.nav.navigate('/search')
const onPressMenu = () => setMainMenuActive(true) const onPressMenu = () => setMainMenuActive(true)
const onPressNotifications = () => store.nav.navigate('/notifications') const onPressNotifications = () => store.nav.navigate('/notifications')
const onPressTabs = () => setTabsSelectorActive(true) const onPressTabs = () => toggleTabsMenu(!isTabsSelectorActive)
const closeTabsSelector = () => setTabsSelectorActive(false)
const toggleTabsMenu = (active: boolean) => {
if (active) {
// will trigger the animation below
setTabsSelectorActive(true)
} else {
tabMenuInterp.value = withTiming(0, {duration: 100}, () => {
// hide once the animation has finished
runOnJS(closeTabsSelector)()
})
}
}
useEffect(() => {
if (isTabsSelectorActive) {
// trigger the animation once the tabs selector is rendering
tabMenuInterp.value = withTiming(1, {duration: 100})
}
}, [isTabsSelectorActive])
const goBack = () => store.nav.tab.goBack() const goBack = () => store.nav.tab.goBack()
const swipeGesture = Gesture.Pan() const swipeGesture = Gesture.Pan()
@ -159,6 +179,9 @@ export const MobileShell: React.FC = observer(() => {
const swipeOpacity = useAnimatedStyle(() => ({ const swipeOpacity = useAnimatedStyle(() => ({
opacity: interpolate(swipeGestureInterp.value, [0, 1.0], [0.6, 0.0]), opacity: interpolate(swipeGestureInterp.value, [0, 1.0], [0.6, 0.0]),
})) }))
const tabMenuTransform = useAnimatedStyle(() => ({
transform: [{translateY: tabMenuInterp.value * -320}],
}))
if (!store.session.isAuthed) { if (!store.session.isAuthed) {
return ( return (
@ -205,7 +228,9 @@ export const MobileShell: React.FC = observer(() => {
style={[ style={[
s.flex1, s.flex1,
styles.screen, styles.screen,
current ? swipeTransform : undefined, current
? [swipeTransform, tabMenuTransform]
: undefined,
]}> ]}>
<Com <Com
params={params} params={params}
@ -220,6 +245,11 @@ export const MobileShell: React.FC = observer(() => {
</ScreenContainer> </ScreenContainer>
</GestureDetector> </GestureDetector>
</SafeAreaView> </SafeAreaView>
<TabsSelector
active={isTabsSelectorActive}
tabMenuInterp={tabMenuInterp}
onClose={() => toggleTabsMenu(false)}
/>
<SafeAreaView style={styles.bottomBar}> <SafeAreaView style={styles.bottomBar}>
<Btn icon="house" onPress={onPressHome} /> <Btn icon="house" onPress={onPressHome} />
<Btn icon="search" onPress={onPressSearch} /> <Btn icon="search" onPress={onPressSearch} />
@ -236,10 +266,6 @@ export const MobileShell: React.FC = observer(() => {
onClose={() => setMainMenuActive(false)} onClose={() => setMainMenuActive(false)}
/> />
<Modal /> <Modal />
<TabsSelector
active={isTabsSelectorActive}
onClose={() => setTabsSelectorActive(false)}
/>
<Composer <Composer
active={store.shell.isComposerActive} active={store.shell.isComposerActive}
onClose={() => store.shell.closeComposer()} onClose={() => store.shell.closeComposer()}