d054a69bcf
* Rework 'navIdx' variables from number arrays to strings to avoid equality-check failures in react hooks * Resolve all remaining lint issues * Fix tests * Use node v18 in gh action test
155 lines
3.9 KiB
TypeScript
155 lines
3.9 KiB
TypeScript
import React, {useState} from 'react'
|
|
import {
|
|
Animated,
|
|
GestureResponderEvent,
|
|
I18nManager,
|
|
PanResponder,
|
|
PanResponderGestureState,
|
|
useWindowDimensions,
|
|
View,
|
|
} from 'react-native'
|
|
import {clamp} from 'lodash'
|
|
import {s} from '../../../lib/styles'
|
|
|
|
interface Props {
|
|
panX: Animated.Value
|
|
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
|
|
onSwipeStartDirection?: (dx: number) => void
|
|
onSwipeEnd?: (dx: number) => void
|
|
children: React.ReactNode
|
|
}
|
|
|
|
export function HorzSwipe({
|
|
panX,
|
|
canSwipeLeft = false,
|
|
canSwipeRight = false,
|
|
swipeEnabled = true,
|
|
hasPriority = false,
|
|
distThresholdDivisor = 1.75,
|
|
useNativeDriver = false,
|
|
onSwipeStart,
|
|
onSwipeStartDirection,
|
|
onSwipeEnd,
|
|
children,
|
|
}: Props) {
|
|
const winDim = useWindowDimensions()
|
|
const [dir, setDir] = useState<number>(0)
|
|
|
|
const swipeVelocityThreshold = 35
|
|
const swipeDistanceThreshold = winDim.width / distThresholdDivisor
|
|
|
|
const isMovingHorizontally = (
|
|
_: GestureResponderEvent,
|
|
gestureState: PanResponderGestureState,
|
|
) => {
|
|
return (
|
|
Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 1.25) &&
|
|
Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 1.25)
|
|
)
|
|
}
|
|
|
|
const canMoveScreen = (
|
|
event: GestureResponderEvent,
|
|
gestureState: PanResponderGestureState,
|
|
) => {
|
|
if (swipeEnabled === false) {
|
|
return false
|
|
}
|
|
|
|
const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
|
|
const willHandle =
|
|
isMovingHorizontally(event, gestureState) &&
|
|
((diffX > 0 && canSwipeLeft) || (diffX < 0 && canSwipeRight))
|
|
return willHandle
|
|
}
|
|
|
|
const startGesture = () => {
|
|
setDir(0)
|
|
onSwipeStart?.()
|
|
|
|
panX.stopAnimation()
|
|
// @ts-expect-error: _value is private, but docs use it as well
|
|
panX.setOffset(panX._value)
|
|
}
|
|
|
|
const respondToGesture = (
|
|
_: GestureResponderEvent,
|
|
gestureState: PanResponderGestureState,
|
|
) => {
|
|
const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
|
|
|
|
if (
|
|
// swiping left
|
|
(diffX > 0 && !canSwipeLeft) ||
|
|
// swiping right
|
|
(diffX < 0 && !canSwipeRight)
|
|
) {
|
|
return
|
|
}
|
|
|
|
panX.setValue(clamp(diffX / swipeDistanceThreshold, -1, 1) * -1)
|
|
|
|
const newDir = diffX > 0 ? -1 : diffX < 0 ? 1 : 0
|
|
if (newDir !== dir) {
|
|
setDir(newDir)
|
|
onSwipeStartDirection?.(newDir)
|
|
}
|
|
}
|
|
|
|
const finishGesture = (
|
|
_: GestureResponderEvent,
|
|
gestureState: PanResponderGestureState,
|
|
) => {
|
|
if (
|
|
Math.abs(gestureState.dx) > Math.abs(gestureState.dy) &&
|
|
Math.abs(gestureState.vx) > Math.abs(gestureState.vy) &&
|
|
(Math.abs(gestureState.dx) > swipeDistanceThreshold / 4 ||
|
|
Math.abs(gestureState.vx) > swipeVelocityThreshold)
|
|
) {
|
|
const final = Math.floor(
|
|
(gestureState.dx / Math.abs(gestureState.dx)) * -1,
|
|
)
|
|
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,
|
|
onPanResponderGrant: startGesture,
|
|
onPanResponderMove: respondToGesture,
|
|
onPanResponderTerminate: finishGesture,
|
|
onPanResponderRelease: finishGesture,
|
|
onPanResponderTerminationRequest: () => !hasPriority,
|
|
})
|
|
|
|
return (
|
|
<View {...panResponder.panHandlers} style={s.h100pct}>
|
|
{children}
|
|
</View>
|
|
)
|
|
}
|