Replace navigational 'back swipe' gesture with new HorzSwipe util
parent
9ce02dff5b
commit
f5d1a5c38d
|
@ -76,6 +76,7 @@ export function ViewSelector({
|
|||
const data = [HEADER_ITEM, SELECTOR_ITEM, ...items]
|
||||
return (
|
||||
<HorzSwipe
|
||||
hasPriority
|
||||
panX={panX}
|
||||
swipeEnabled={swipeEnabled || false}
|
||||
canSwipeLeft={selectedIndex > 0}
|
||||
|
|
|
@ -12,9 +12,12 @@ import {clamp} from 'lodash'
|
|||
|
||||
interface Props {
|
||||
panX: Animated.Value
|
||||
canSwipeLeft: boolean
|
||||
canSwipeRight: boolean
|
||||
swipeEnabled: boolean
|
||||
canSwipeLeft?: boolean
|
||||
canSwipeRight?: boolean
|
||||
swipeEnabled?: boolean
|
||||
hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture
|
||||
distThresholdDivisor?: number
|
||||
useNativeDriver?: boolean
|
||||
onSwipeStart?: () => void
|
||||
onSwipeEnd?: (dx: number) => void
|
||||
children: React.ReactNode
|
||||
|
@ -22,9 +25,12 @@ interface Props {
|
|||
|
||||
export function HorzSwipe({
|
||||
panX,
|
||||
canSwipeLeft,
|
||||
canSwipeRight,
|
||||
canSwipeLeft = false,
|
||||
canSwipeRight = false,
|
||||
swipeEnabled = true,
|
||||
hasPriority = false,
|
||||
distThresholdDivisor = 1.75,
|
||||
useNativeDriver = false,
|
||||
onSwipeStart,
|
||||
onSwipeEnd,
|
||||
children,
|
||||
|
@ -32,7 +38,7 @@ export function HorzSwipe({
|
|||
const winDim = useWindowDimensions()
|
||||
|
||||
const swipeVelocityThreshold = 35
|
||||
const swipeDistanceThreshold = winDim.width / 1.75
|
||||
const swipeDistanceThreshold = winDim.width / distThresholdDivisor
|
||||
|
||||
const isMovingHorizontally = (
|
||||
_: GestureResponderEvent,
|
||||
|
@ -53,10 +59,10 @@ export function HorzSwipe({
|
|||
}
|
||||
|
||||
const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
|
||||
return (
|
||||
const willHandle =
|
||||
isMovingHorizontally(event, gestureState) &&
|
||||
((diffX > 0 && canSwipeLeft) || (diffX < 0 && canSwipeRight))
|
||||
)
|
||||
return willHandle
|
||||
}
|
||||
|
||||
const startGesture = () => {
|
||||
|
@ -94,28 +100,42 @@ export function HorzSwipe({
|
|||
_: GestureResponderEvent,
|
||||
gestureState: PanResponderGestureState,
|
||||
) => {
|
||||
panX.flattenOffset()
|
||||
panX.setValue(0)
|
||||
if (
|
||||
Math.abs(gestureState.dx) > Math.abs(gestureState.dy) &&
|
||||
Math.abs(gestureState.vx) > Math.abs(gestureState.vy) &&
|
||||
(Math.abs(gestureState.dx) > swipeDistanceThreshold / 3 ||
|
||||
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 {
|
||||
onSwipeEnd?.(0)
|
||||
Animated.timing(panX, {
|
||||
toValue: 0,
|
||||
duration: 100,
|
||||
useNativeDriver,
|
||||
}).start(() => {
|
||||
panX.flattenOffset()
|
||||
panX.setValue(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const panResponder = PanResponder.create({
|
||||
onMoveShouldSetPanResponder: canMoveScreen,
|
||||
onMoveShouldSetPanResponderCapture: canMoveScreen,
|
||||
onPanResponderGrant: startGesture,
|
||||
onPanResponderMove: respondToGesture,
|
||||
onPanResponderTerminate: finishGesture,
|
||||
onPanResponderRelease: finishGesture,
|
||||
onPanResponderTerminationRequest: () => true,
|
||||
onPanResponderTerminationRequest: () => !hasPriority,
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, {useState, useEffect, useRef} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
useWindowDimensions,
|
||||
Animated as RNAnimated,
|
||||
FlatList,
|
||||
GestureResponderEvent,
|
||||
SafeAreaView,
|
||||
|
@ -9,12 +9,12 @@ import {
|
|||
Text,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {ScreenContainer, Screen} from 'react-native-screens'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {GestureDetector, Gesture} from 'react-native-gesture-handler'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import Animated, {
|
||||
Easing,
|
||||
|
@ -32,6 +32,7 @@ import {NavigationModel} from '../../../state/models/navigation'
|
|||
import {match, MatchResult} from '../../routes'
|
||||
import {Login} from '../../screens/Login'
|
||||
import {Onboard} from '../../screens/Onboard'
|
||||
import {HorzSwipe} from '../../com/util/gestures/HorzSwipe'
|
||||
import {Modal} from '../../com/modals/Modal'
|
||||
import {TabsSelector} from './TabsSelector'
|
||||
import {Composer} from './Composer'
|
||||
|
@ -45,9 +46,7 @@ import {
|
|||
BellIcon,
|
||||
BellIconSolid,
|
||||
} from '../../lib/icons'
|
||||
|
||||
const SWIPE_GESTURE_DIST_TRIGGER = 0.3
|
||||
const SWIPE_GESTURE_VEL_TRIGGER = 2000
|
||||
import {useAnimatedValue} from '../../lib/useAnimatedValue'
|
||||
|
||||
const Btn = ({
|
||||
icon,
|
||||
|
@ -120,7 +119,7 @@ export const MobileShell: React.FC = observer(() => {
|
|||
const [isTabsSelectorActive, setTabsSelectorActive] = useState(false)
|
||||
const scrollElRef = useRef<FlatList | undefined>()
|
||||
const winDim = useWindowDimensions()
|
||||
const swipeGestureInterp = useSharedValue<number>(0)
|
||||
const swipeGestureInterp = useAnimatedValue(0)
|
||||
const tabMenuInterp = useSharedValue<number>(0)
|
||||
const newTabInterp = useSharedValue<number>(0)
|
||||
const [isRunningNewTabAnim, setIsRunningNewTabAnim] = useState(false)
|
||||
|
@ -185,37 +184,22 @@ export const MobileShell: React.FC = observer(() => {
|
|||
|
||||
// navigation swipes
|
||||
// =
|
||||
const goBack = () => store.nav.tab.goBack()
|
||||
const swipeGesture = Gesture.Pan()
|
||||
.enabled(store.nav.tab.canGoBack)
|
||||
.onUpdate(e => {
|
||||
if (store.nav.tab.canGoBack) {
|
||||
swipeGestureInterp.value = Math.max(e.translationX / winDim.width, 0)
|
||||
}
|
||||
})
|
||||
.onEnd(e => {
|
||||
if (
|
||||
swipeGestureInterp.value >= SWIPE_GESTURE_DIST_TRIGGER ||
|
||||
e.velocityX > SWIPE_GESTURE_VEL_TRIGGER
|
||||
) {
|
||||
swipeGestureInterp.value = withTiming(1, {duration: 100}, () => {
|
||||
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 onNavSwipeEnd = (dx: number) => {
|
||||
if (dx < 0 && store.nav.tab.canGoBack) {
|
||||
store.nav.tab.goBack()
|
||||
}
|
||||
}
|
||||
const swipeTransform = {
|
||||
transform: [
|
||||
{translateX: RNAnimated.multiply(swipeGestureInterp, winDim.width * -1)},
|
||||
],
|
||||
}
|
||||
const swipeOpacity = {
|
||||
opacity: swipeGestureInterp.interpolate({
|
||||
inputRange: [-1, 0, 1],
|
||||
outputRange: [0, 0.6, 0],
|
||||
}),
|
||||
}
|
||||
const tabMenuTransform = useAnimatedStyle(() => ({
|
||||
transform: [{translateY: tabMenuInterp.value * -320}],
|
||||
}))
|
||||
|
@ -252,7 +236,13 @@ export const MobileShell: React.FC = observer(() => {
|
|||
return (
|
||||
<View style={styles.outerContainer}>
|
||||
<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}>
|
||||
{screenRenderDesc.screens.map(
|
||||
({Com, navIdx, params, key, current, previous}) => {
|
||||
|
@ -261,20 +251,20 @@ export const MobileShell: React.FC = observer(() => {
|
|||
key={key}
|
||||
style={[StyleSheet.absoluteFill]}
|
||||
activityState={current ? 2 : previous ? 1 : 0}>
|
||||
<Animated.View
|
||||
<RNAnimated.View
|
||||
style={
|
||||
current ? [styles.screenMask, swipeOpacity] : undefined
|
||||
}
|
||||
/>
|
||||
<Animated.View
|
||||
<RNAnimated.View
|
||||
style={[
|
||||
s.flex1,
|
||||
styles.screen,
|
||||
current
|
||||
? [
|
||||
swipeTransform,
|
||||
tabMenuTransform,
|
||||
isRunningNewTabAnim ? newTabTransform : undefined,
|
||||
// tabMenuTransform, TODO
|
||||
// isRunningNewTabAnim ? newTabTransform : undefined, TODO
|
||||
]
|
||||
: undefined,
|
||||
]}>
|
||||
|
@ -284,13 +274,13 @@ export const MobileShell: React.FC = observer(() => {
|
|||
visible={current}
|
||||
scrollElRef={current ? scrollElRef : undefined}
|
||||
/>
|
||||
</Animated.View>
|
||||
</RNAnimated.View>
|
||||
</Screen>
|
||||
)
|
||||
},
|
||||
)}
|
||||
</ScreenContainer>
|
||||
</GestureDetector>
|
||||
</HorzSwipe>
|
||||
</SafeAreaView>
|
||||
{isTabsSelectorActive ? (
|
||||
<View
|
||||
|
|
Loading…
Reference in New Issue