mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Moved submodule to codebase
Due to using new dependencies such as `react-native-gesture-handler`
This commit is contained in:
407
src/screens/ImageViewer/hooks/usePanResponder.ts
Normal file
407
src/screens/ImageViewer/hooks/usePanResponder.ts
Normal file
@ -0,0 +1,407 @@
|
||||
/**
|
||||
* Copyright (c) JOB TODAY S.A. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import { useMemo, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
GestureResponderEvent,
|
||||
GestureResponderHandlers,
|
||||
NativeTouchEvent,
|
||||
PanResponderGestureState
|
||||
} from 'react-native'
|
||||
import { Position } from '../@types'
|
||||
import {
|
||||
createPanResponder,
|
||||
getDistanceBetweenTouches,
|
||||
getImageTranslate,
|
||||
getImageDimensionsByTranslate
|
||||
} from '../utils'
|
||||
|
||||
const SCREEN = Dimensions.get('window')
|
||||
const SCREEN_WIDTH = SCREEN.width
|
||||
const SCREEN_HEIGHT = SCREEN.height
|
||||
const MIN_DIMENSION = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT)
|
||||
|
||||
const SCALE_MAX = 1
|
||||
const DOUBLE_TAP_DELAY = 300
|
||||
const OUT_BOUND_MULTIPLIER = 0.75
|
||||
|
||||
type Props = {
|
||||
initialScale: number
|
||||
initialTranslate: Position
|
||||
onZoom: (isZoomed: boolean) => void
|
||||
doubleTapToZoomEnabled: boolean
|
||||
onLongPress: () => void
|
||||
delayLongPress: number
|
||||
onRequestClose: () => void
|
||||
}
|
||||
|
||||
const usePanResponder = ({
|
||||
initialScale,
|
||||
initialTranslate,
|
||||
onZoom,
|
||||
doubleTapToZoomEnabled,
|
||||
onLongPress,
|
||||
delayLongPress,
|
||||
onRequestClose
|
||||
}: Props): Readonly<[
|
||||
GestureResponderHandlers,
|
||||
Animated.Value,
|
||||
Animated.ValueXY
|
||||
]> => {
|
||||
let numberInitialTouches = 1
|
||||
let initialTouches: NativeTouchEvent[] = []
|
||||
let currentScale = initialScale
|
||||
let currentTranslate = initialTranslate
|
||||
let tmpScale = 0
|
||||
let tmpTranslate: Position | null = null
|
||||
let isDoubleTapPerformed = false
|
||||
let lastTapTS: number | null = null
|
||||
let timer: number | null = null
|
||||
let longPressHandlerRef: number | null = null
|
||||
|
||||
const meaningfulShift = MIN_DIMENSION * 0.01
|
||||
const scaleValue = new Animated.Value(initialScale)
|
||||
const translateValue = new Animated.ValueXY(initialTranslate)
|
||||
|
||||
const imageDimensions = getImageDimensionsByTranslate(
|
||||
initialTranslate,
|
||||
SCREEN
|
||||
)
|
||||
|
||||
const getBounds = (scale: number) => {
|
||||
const scaledImageDimensions = {
|
||||
width: imageDimensions.width * scale,
|
||||
height: imageDimensions.height * scale
|
||||
}
|
||||
const translateDelta = getImageTranslate(scaledImageDimensions, SCREEN)
|
||||
|
||||
const left = initialTranslate.x - translateDelta.x
|
||||
const right = left - (scaledImageDimensions.width - SCREEN.width)
|
||||
const top = initialTranslate.y - translateDelta.y
|
||||
const bottom = top - (scaledImageDimensions.height - SCREEN.height)
|
||||
|
||||
return [top, left, bottom, right]
|
||||
}
|
||||
|
||||
const getTranslateInBounds = (translate: Position, scale: number) => {
|
||||
const inBoundTranslate = { x: translate.x, y: translate.y }
|
||||
const [topBound, leftBound, bottomBound, rightBound] = getBounds(scale)
|
||||
|
||||
if (translate.x > leftBound) {
|
||||
inBoundTranslate.x = leftBound
|
||||
} else if (translate.x < rightBound) {
|
||||
inBoundTranslate.x = rightBound
|
||||
}
|
||||
|
||||
if (translate.y > topBound) {
|
||||
inBoundTranslate.y = topBound
|
||||
} else if (translate.y < bottomBound) {
|
||||
inBoundTranslate.y = bottomBound
|
||||
}
|
||||
|
||||
return inBoundTranslate
|
||||
}
|
||||
|
||||
const fitsScreenByWidth = () =>
|
||||
imageDimensions.width * currentScale < SCREEN_WIDTH
|
||||
const fitsScreenByHeight = () =>
|
||||
imageDimensions.height * currentScale < SCREEN_HEIGHT
|
||||
|
||||
useEffect(() => {
|
||||
scaleValue.addListener(({ value }) => {
|
||||
if (typeof onZoom === 'function') {
|
||||
onZoom(value !== initialScale)
|
||||
}
|
||||
})
|
||||
|
||||
return () => scaleValue.removeAllListeners()
|
||||
})
|
||||
|
||||
const cancelLongPressHandle = () => {
|
||||
longPressHandlerRef && clearTimeout(longPressHandlerRef)
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
onGrant: (
|
||||
_: GestureResponderEvent,
|
||||
gestureState: PanResponderGestureState
|
||||
) => {
|
||||
numberInitialTouches = gestureState.numberActiveTouches
|
||||
|
||||
if (gestureState.numberActiveTouches > 1) return
|
||||
|
||||
longPressHandlerRef = setTimeout(onLongPress, delayLongPress)
|
||||
},
|
||||
onStart: (
|
||||
event: GestureResponderEvent,
|
||||
gestureState: PanResponderGestureState
|
||||
) => {
|
||||
initialTouches = event.nativeEvent.touches
|
||||
numberInitialTouches = gestureState.numberActiveTouches
|
||||
|
||||
if (gestureState.numberActiveTouches > 1) return
|
||||
|
||||
const tapTS = Date.now()
|
||||
!timer &&
|
||||
(timer = setTimeout(() => onRequestClose(), DOUBLE_TAP_DELAY + 50))
|
||||
// Handle double tap event by calculating diff between first and second taps timestamps
|
||||
|
||||
isDoubleTapPerformed = Boolean(
|
||||
lastTapTS && tapTS - lastTapTS < DOUBLE_TAP_DELAY
|
||||
)
|
||||
|
||||
if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
|
||||
clearTimeout(timer)
|
||||
const isScaled = currentTranslate.x !== initialTranslate.x // currentScale !== initialScale;
|
||||
const { pageX: touchX, pageY: touchY } = event.nativeEvent.touches[0]
|
||||
const targetScale = SCALE_MAX
|
||||
const nextScale = isScaled ? initialScale : targetScale
|
||||
const nextTranslate = isScaled
|
||||
? initialTranslate
|
||||
: getTranslateInBounds(
|
||||
{
|
||||
x:
|
||||
initialTranslate.x +
|
||||
(SCREEN_WIDTH / 2 - touchX) * (targetScale / currentScale),
|
||||
y:
|
||||
initialTranslate.y +
|
||||
(SCREEN_HEIGHT / 2 - -touchY) * (targetScale / currentScale)
|
||||
},
|
||||
targetScale
|
||||
)
|
||||
|
||||
onZoom(!isScaled)
|
||||
|
||||
Animated.parallel(
|
||||
[
|
||||
Animated.timing(translateValue.x, {
|
||||
toValue: nextTranslate.x,
|
||||
duration: 300,
|
||||
useNativeDriver: true
|
||||
}),
|
||||
Animated.timing(translateValue.y, {
|
||||
toValue: nextTranslate.y,
|
||||
duration: 300,
|
||||
useNativeDriver: true
|
||||
}),
|
||||
Animated.timing(scaleValue, {
|
||||
toValue: nextScale,
|
||||
duration: 300,
|
||||
useNativeDriver: true
|
||||
})
|
||||
],
|
||||
{ stopTogether: false }
|
||||
).start(() => {
|
||||
currentScale = nextScale
|
||||
currentTranslate = nextTranslate
|
||||
})
|
||||
|
||||
lastTapTS = null
|
||||
timer = null
|
||||
} else {
|
||||
lastTapTS = Date.now()
|
||||
}
|
||||
},
|
||||
onMove: (
|
||||
event: GestureResponderEvent,
|
||||
gestureState: PanResponderGestureState
|
||||
) => {
|
||||
const { dx, dy } = gestureState
|
||||
|
||||
if (Math.abs(dx) >= meaningfulShift || Math.abs(dy) >= meaningfulShift) {
|
||||
cancelLongPressHandle()
|
||||
timer && clearTimeout(timer)
|
||||
}
|
||||
|
||||
// Don't need to handle move because double tap in progress (was handled in onStart)
|
||||
if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
|
||||
cancelLongPressHandle()
|
||||
timer && clearTimeout(timer)
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
numberInitialTouches === 1 &&
|
||||
gestureState.numberActiveTouches === 2
|
||||
) {
|
||||
numberInitialTouches = 2
|
||||
initialTouches = event.nativeEvent.touches
|
||||
}
|
||||
|
||||
const isTapGesture =
|
||||
numberInitialTouches == 1 && gestureState.numberActiveTouches === 1
|
||||
const isPinchGesture =
|
||||
numberInitialTouches === 2 && gestureState.numberActiveTouches === 2
|
||||
|
||||
if (isPinchGesture) {
|
||||
cancelLongPressHandle()
|
||||
timer && clearTimeout(timer)
|
||||
|
||||
const initialDistance = getDistanceBetweenTouches(initialTouches)
|
||||
const currentDistance = getDistanceBetweenTouches(
|
||||
event.nativeEvent.touches
|
||||
)
|
||||
|
||||
let nextScale = (currentDistance / initialDistance) * currentScale
|
||||
|
||||
/**
|
||||
* In case image is scaling smaller than initial size ->
|
||||
* slow down this transition by applying OUT_BOUND_MULTIPLIER
|
||||
*/
|
||||
if (nextScale < initialScale) {
|
||||
nextScale =
|
||||
nextScale + (initialScale - nextScale) * OUT_BOUND_MULTIPLIER
|
||||
}
|
||||
|
||||
/**
|
||||
* In case image is scaling down -> move it in direction of initial position
|
||||
*/
|
||||
if (currentScale > initialScale && currentScale > nextScale) {
|
||||
const k = (currentScale - initialScale) / (currentScale - nextScale)
|
||||
|
||||
const nextTranslateX =
|
||||
nextScale < initialScale
|
||||
? initialTranslate.x
|
||||
: currentTranslate.x -
|
||||
(currentTranslate.x - initialTranslate.x) / k
|
||||
|
||||
const nextTranslateY =
|
||||
nextScale < initialScale
|
||||
? initialTranslate.y
|
||||
: currentTranslate.y -
|
||||
(currentTranslate.y - initialTranslate.y) / k
|
||||
|
||||
translateValue.x.setValue(nextTranslateX)
|
||||
translateValue.y.setValue(nextTranslateY)
|
||||
|
||||
tmpTranslate = { x: nextTranslateX, y: nextTranslateY }
|
||||
}
|
||||
|
||||
scaleValue.setValue(nextScale)
|
||||
tmpScale = nextScale
|
||||
}
|
||||
|
||||
if (isTapGesture && currentScale > initialScale) {
|
||||
const { x, y } = currentTranslate
|
||||
const { dx, dy } = gestureState
|
||||
const [topBound, leftBound, bottomBound, rightBound] = getBounds(
|
||||
currentScale
|
||||
)
|
||||
|
||||
let nextTranslateX = x + dx
|
||||
let nextTranslateY = y + dy
|
||||
|
||||
if (nextTranslateX > leftBound) {
|
||||
nextTranslateX =
|
||||
nextTranslateX - (nextTranslateX - leftBound) * OUT_BOUND_MULTIPLIER
|
||||
}
|
||||
|
||||
if (nextTranslateX < rightBound) {
|
||||
nextTranslateX =
|
||||
nextTranslateX -
|
||||
(nextTranslateX - rightBound) * OUT_BOUND_MULTIPLIER
|
||||
}
|
||||
|
||||
if (nextTranslateY > topBound) {
|
||||
nextTranslateY =
|
||||
nextTranslateY - (nextTranslateY - topBound) * OUT_BOUND_MULTIPLIER
|
||||
}
|
||||
|
||||
if (nextTranslateY < bottomBound) {
|
||||
nextTranslateY =
|
||||
nextTranslateY -
|
||||
(nextTranslateY - bottomBound) * OUT_BOUND_MULTIPLIER
|
||||
}
|
||||
|
||||
if (fitsScreenByWidth()) {
|
||||
nextTranslateX = x
|
||||
}
|
||||
|
||||
if (fitsScreenByHeight()) {
|
||||
nextTranslateY = y
|
||||
}
|
||||
|
||||
translateValue.x.setValue(nextTranslateX)
|
||||
translateValue.y.setValue(nextTranslateY)
|
||||
|
||||
tmpTranslate = { x: nextTranslateX, y: nextTranslateY }
|
||||
}
|
||||
},
|
||||
onRelease: () => {
|
||||
cancelLongPressHandle()
|
||||
|
||||
if (isDoubleTapPerformed) {
|
||||
isDoubleTapPerformed = false
|
||||
}
|
||||
|
||||
if (tmpScale > 0) {
|
||||
if (tmpScale < initialScale || tmpScale > SCALE_MAX) {
|
||||
tmpScale = tmpScale < initialScale ? initialScale : SCALE_MAX
|
||||
Animated.timing(scaleValue, {
|
||||
toValue: tmpScale,
|
||||
duration: 100,
|
||||
useNativeDriver: true
|
||||
}).start()
|
||||
}
|
||||
|
||||
currentScale = tmpScale
|
||||
tmpScale = 0
|
||||
}
|
||||
|
||||
if (tmpTranslate) {
|
||||
const { x, y } = tmpTranslate
|
||||
const [topBound, leftBound, bottomBound, rightBound] = getBounds(
|
||||
currentScale
|
||||
)
|
||||
|
||||
let nextTranslateX = x
|
||||
let nextTranslateY = y
|
||||
|
||||
if (!fitsScreenByWidth()) {
|
||||
if (nextTranslateX > leftBound) {
|
||||
nextTranslateX = leftBound
|
||||
} else if (nextTranslateX < rightBound) {
|
||||
nextTranslateX = rightBound
|
||||
}
|
||||
}
|
||||
|
||||
if (!fitsScreenByHeight()) {
|
||||
if (nextTranslateY > topBound) {
|
||||
nextTranslateY = topBound
|
||||
} else if (nextTranslateY < bottomBound) {
|
||||
nextTranslateY = bottomBound
|
||||
}
|
||||
}
|
||||
|
||||
Animated.parallel([
|
||||
Animated.timing(translateValue.x, {
|
||||
toValue: nextTranslateX,
|
||||
duration: 100,
|
||||
useNativeDriver: true
|
||||
}),
|
||||
Animated.timing(translateValue.y, {
|
||||
toValue: nextTranslateY,
|
||||
duration: 100,
|
||||
useNativeDriver: true
|
||||
})
|
||||
]).start()
|
||||
|
||||
currentTranslate = { x: nextTranslateX, y: nextTranslateY }
|
||||
tmpTranslate = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const panResponder = useMemo(() => createPanResponder(handlers), [handlers])
|
||||
|
||||
return [panResponder.panHandlers, scaleValue, translateValue]
|
||||
}
|
||||
|
||||
export default usePanResponder
|
Reference in New Issue
Block a user