diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index f7652334..f3925497 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -76,6 +76,7 @@ export function ViewSelector({ const data = [HEADER_ITEM, SELECTOR_ITEM, ...items] return ( 0} diff --git a/src/view/com/util/gestures/HorzSwipe.tsx b/src/view/com/util/gestures/HorzSwipe.tsx index 7ae1ee75..0176bd4c 100644 --- a/src/view/com/util/gestures/HorzSwipe.tsx +++ b/src/view/com/util/gestures/HorzSwipe.tsx @@ -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 ( diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index 6bb11187..ee6c9985 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -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() const winDim = useWindowDimensions() - const swipeGestureInterp = useSharedValue(0) + const swipeGestureInterp = useAnimatedValue(0) const tabMenuInterp = useSharedValue(0) const newTabInterp = useSharedValue(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 ( - + {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}> - - @@ -284,13 +274,13 @@ export const MobileShell: React.FC = observer(() => { visible={current} scrollElRef={current ? scrollElRef : undefined} /> - + ) }, )} - + {isTabsSelectorActive ? (