Files
bsky-social-app/src/view/com/util/gestures/HorzSwipe.tsx
T
Paul Frazee d054a69bcf Resolve all remaining lint issues (#88)
* 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
2023-01-24 13:00:11 -06:00

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>
)
}