Replace navigational 'back swipe' gesture with new HorzSwipe util

zio/stable
Paul Frazee 2022-12-07 16:56:14 -06:00
parent 9ce02dff5b
commit f5d1a5c38d
3 changed files with 68 additions and 57 deletions

View File

@ -76,6 +76,7 @@ export function ViewSelector({
const data = [HEADER_ITEM, SELECTOR_ITEM, ...items] const data = [HEADER_ITEM, SELECTOR_ITEM, ...items]
return ( return (
<HorzSwipe <HorzSwipe
hasPriority
panX={panX} panX={panX}
swipeEnabled={swipeEnabled || false} swipeEnabled={swipeEnabled || false}
canSwipeLeft={selectedIndex > 0} canSwipeLeft={selectedIndex > 0}

View File

@ -12,9 +12,12 @@ import {clamp} from 'lodash'
interface Props { interface Props {
panX: Animated.Value panX: Animated.Value
canSwipeLeft: boolean canSwipeLeft?: boolean
canSwipeRight: boolean canSwipeRight?: boolean
swipeEnabled: boolean swipeEnabled?: boolean
hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture
distThresholdDivisor?: number
useNativeDriver?: boolean
onSwipeStart?: () => void onSwipeStart?: () => void
onSwipeEnd?: (dx: number) => void onSwipeEnd?: (dx: number) => void
children: React.ReactNode children: React.ReactNode
@ -22,9 +25,12 @@ interface Props {
export function HorzSwipe({ export function HorzSwipe({
panX, panX,
canSwipeLeft, canSwipeLeft = false,
canSwipeRight, canSwipeRight = false,
swipeEnabled = true, swipeEnabled = true,
hasPriority = false,
distThresholdDivisor = 1.75,
useNativeDriver = false,
onSwipeStart, onSwipeStart,
onSwipeEnd, onSwipeEnd,
children, children,
@ -32,7 +38,7 @@ export function HorzSwipe({
const winDim = useWindowDimensions() const winDim = useWindowDimensions()
const swipeVelocityThreshold = 35 const swipeVelocityThreshold = 35
const swipeDistanceThreshold = winDim.width / 1.75 const swipeDistanceThreshold = winDim.width / distThresholdDivisor
const isMovingHorizontally = ( const isMovingHorizontally = (
_: GestureResponderEvent, _: GestureResponderEvent,
@ -53,10 +59,10 @@ export function HorzSwipe({
} }
const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
return ( const willHandle =
isMovingHorizontally(event, gestureState) && isMovingHorizontally(event, gestureState) &&
((diffX > 0 && canSwipeLeft) || (diffX < 0 && canSwipeRight)) ((diffX > 0 && canSwipeLeft) || (diffX < 0 && canSwipeRight))
) return willHandle
} }
const startGesture = () => { const startGesture = () => {
@ -94,28 +100,42 @@ export function HorzSwipe({
_: GestureResponderEvent, _: GestureResponderEvent,
gestureState: PanResponderGestureState, gestureState: PanResponderGestureState,
) => { ) => {
panX.flattenOffset()
panX.setValue(0)
if ( if (
Math.abs(gestureState.dx) > Math.abs(gestureState.dy) && Math.abs(gestureState.dx) > Math.abs(gestureState.dy) &&
Math.abs(gestureState.vx) > Math.abs(gestureState.vy) && Math.abs(gestureState.vx) > Math.abs(gestureState.vy) &&
(Math.abs(gestureState.dx) > swipeDistanceThreshold / 3 || (Math.abs(gestureState.dx) > swipeDistanceThreshold / 3 ||
Math.abs(gestureState.vx) > swipeVelocityThreshold) Math.abs(gestureState.vx) > swipeVelocityThreshold)
) { ) {
onSwipeEnd?.(((gestureState.dx / Math.abs(gestureState.dx)) * -1) | 0) const final = ((gestureState.dx / Math.abs(gestureState.dx)) * -1) | 0
Animated.timing(panX, {
toValue: final,
duration: 100,
useNativeDriver,
}).start(() => {
onSwipeEnd?.(final)
panX.flattenOffset()
panX.setValue(0)
})
} else { } else {
onSwipeEnd?.(0) onSwipeEnd?.(0)
Animated.timing(panX, {
toValue: 0,
duration: 100,
useNativeDriver,
}).start(() => {
panX.flattenOffset()
panX.setValue(0)
})
} }
} }
const panResponder = PanResponder.create({ const panResponder = PanResponder.create({
onMoveShouldSetPanResponder: canMoveScreen, onMoveShouldSetPanResponder: canMoveScreen,
onMoveShouldSetPanResponderCapture: canMoveScreen,
onPanResponderGrant: startGesture, onPanResponderGrant: startGesture,
onPanResponderMove: respondToGesture, onPanResponderMove: respondToGesture,
onPanResponderTerminate: finishGesture, onPanResponderTerminate: finishGesture,
onPanResponderRelease: finishGesture, onPanResponderRelease: finishGesture,
onPanResponderTerminationRequest: () => true, onPanResponderTerminationRequest: () => !hasPriority,
}) })
return ( return (

View File

@ -1,7 +1,7 @@
import React, {useState, useEffect, useRef} from 'react' import React, {useState, useEffect, useRef} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import { import {
useWindowDimensions, Animated as RNAnimated,
FlatList, FlatList,
GestureResponderEvent, GestureResponderEvent,
SafeAreaView, SafeAreaView,
@ -9,12 +9,12 @@ import {
Text, Text,
TouchableOpacity, TouchableOpacity,
useColorScheme, useColorScheme,
useWindowDimensions,
View, View,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {ScreenContainer, Screen} from 'react-native-screens' import {ScreenContainer, Screen} from 'react-native-screens'
import LinearGradient from 'react-native-linear-gradient' import LinearGradient from 'react-native-linear-gradient'
import {GestureDetector, Gesture} from 'react-native-gesture-handler'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Animated, { import Animated, {
Easing, Easing,
@ -32,6 +32,7 @@ import {NavigationModel} from '../../../state/models/navigation'
import {match, MatchResult} from '../../routes' import {match, MatchResult} from '../../routes'
import {Login} from '../../screens/Login' import {Login} from '../../screens/Login'
import {Onboard} from '../../screens/Onboard' import {Onboard} from '../../screens/Onboard'
import {HorzSwipe} from '../../com/util/gestures/HorzSwipe'
import {Modal} from '../../com/modals/Modal' import {Modal} from '../../com/modals/Modal'
import {TabsSelector} from './TabsSelector' import {TabsSelector} from './TabsSelector'
import {Composer} from './Composer' import {Composer} from './Composer'
@ -45,9 +46,7 @@ import {
BellIcon, BellIcon,
BellIconSolid, BellIconSolid,
} from '../../lib/icons' } from '../../lib/icons'
import {useAnimatedValue} from '../../lib/useAnimatedValue'
const SWIPE_GESTURE_DIST_TRIGGER = 0.3
const SWIPE_GESTURE_VEL_TRIGGER = 2000
const Btn = ({ const Btn = ({
icon, icon,
@ -120,7 +119,7 @@ export const MobileShell: React.FC = observer(() => {
const [isTabsSelectorActive, setTabsSelectorActive] = useState(false) const [isTabsSelectorActive, setTabsSelectorActive] = useState(false)
const scrollElRef = useRef<FlatList | undefined>() const scrollElRef = useRef<FlatList | undefined>()
const winDim = useWindowDimensions() const winDim = useWindowDimensions()
const swipeGestureInterp = useSharedValue<number>(0) const swipeGestureInterp = useAnimatedValue(0)
const tabMenuInterp = useSharedValue<number>(0) const tabMenuInterp = useSharedValue<number>(0)
const newTabInterp = useSharedValue<number>(0) const newTabInterp = useSharedValue<number>(0)
const [isRunningNewTabAnim, setIsRunningNewTabAnim] = useState(false) const [isRunningNewTabAnim, setIsRunningNewTabAnim] = useState(false)
@ -185,37 +184,22 @@ export const MobileShell: React.FC = observer(() => {
// navigation swipes // navigation swipes
// = // =
const goBack = () => store.nav.tab.goBack() const onNavSwipeEnd = (dx: number) => {
const swipeGesture = Gesture.Pan() if (dx < 0 && store.nav.tab.canGoBack) {
.enabled(store.nav.tab.canGoBack) store.nav.tab.goBack()
.onUpdate(e => { }
if (store.nav.tab.canGoBack) { }
swipeGestureInterp.value = Math.max(e.translationX / winDim.width, 0) const swipeTransform = {
} transform: [
}) {translateX: RNAnimated.multiply(swipeGestureInterp, winDim.width * -1)},
.onEnd(e => { ],
if ( }
swipeGestureInterp.value >= SWIPE_GESTURE_DIST_TRIGGER || const swipeOpacity = {
e.velocityX > SWIPE_GESTURE_VEL_TRIGGER opacity: swipeGestureInterp.interpolate({
) { inputRange: [-1, 0, 1],
swipeGestureInterp.value = withTiming(1, {duration: 100}, () => { outputRange: [0, 0.6, 0],
runOnJS(goBack)() }),
}) }
} else {
swipeGestureInterp.value = withTiming(0, {duration: 100})
}
})
useEffect(() => {
// reset the swipe interopolation when the page changes
swipeGestureInterp.value = 0
}, [swipeGestureInterp, store.nav.tab.current])
const swipeTransform = useAnimatedStyle(() => ({
transform: [{translateX: swipeGestureInterp.value * winDim.width}],
}))
const swipeOpacity = useAnimatedStyle(() => ({
opacity: interpolate(swipeGestureInterp.value, [0, 1.0], [0.6, 0.0]),
}))
const tabMenuTransform = useAnimatedStyle(() => ({ const tabMenuTransform = useAnimatedStyle(() => ({
transform: [{translateY: tabMenuInterp.value * -320}], transform: [{translateY: tabMenuInterp.value * -320}],
})) }))
@ -252,7 +236,13 @@ export const MobileShell: React.FC = observer(() => {
return ( return (
<View style={styles.outerContainer}> <View style={styles.outerContainer}>
<SafeAreaView style={styles.innerContainer}> <SafeAreaView style={styles.innerContainer}>
<GestureDetector gesture={swipeGesture}> <HorzSwipe
distThresholdDivisor={1.5}
useNativeDriver
panX={swipeGestureInterp}
swipeEnabled
canSwipeLeft={store.nav.tab.canGoBack}
onSwipeEnd={onNavSwipeEnd}>
<ScreenContainer style={styles.screenContainer}> <ScreenContainer style={styles.screenContainer}>
{screenRenderDesc.screens.map( {screenRenderDesc.screens.map(
({Com, navIdx, params, key, current, previous}) => { ({Com, navIdx, params, key, current, previous}) => {
@ -261,20 +251,20 @@ export const MobileShell: React.FC = observer(() => {
key={key} key={key}
style={[StyleSheet.absoluteFill]} style={[StyleSheet.absoluteFill]}
activityState={current ? 2 : previous ? 1 : 0}> activityState={current ? 2 : previous ? 1 : 0}>
<Animated.View <RNAnimated.View
style={ style={
current ? [styles.screenMask, swipeOpacity] : undefined current ? [styles.screenMask, swipeOpacity] : undefined
} }
/> />
<Animated.View <RNAnimated.View
style={[ style={[
s.flex1, s.flex1,
styles.screen, styles.screen,
current current
? [ ? [
swipeTransform, swipeTransform,
tabMenuTransform, // tabMenuTransform, TODO
isRunningNewTabAnim ? newTabTransform : undefined, // isRunningNewTabAnim ? newTabTransform : undefined, TODO
] ]
: undefined, : undefined,
]}> ]}>
@ -284,13 +274,13 @@ export const MobileShell: React.FC = observer(() => {
visible={current} visible={current}
scrollElRef={current ? scrollElRef : undefined} scrollElRef={current ? scrollElRef : undefined}
/> />
</Animated.View> </RNAnimated.View>
</Screen> </Screen>
) )
}, },
)} )}
</ScreenContainer> </ScreenContainer>
</GestureDetector> </HorzSwipe>
</SafeAreaView> </SafeAreaView>
{isTabsSelectorActive ? ( {isTabsSelectorActive ? (
<View <View