diff options
Diffstat (limited to 'src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts')
-rw-r--r-- | src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts | 423 |
1 files changed, 0 insertions, 423 deletions
diff --git a/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts b/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts deleted file mode 100644 index 85454e37e..000000000 --- a/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts +++ /dev/null @@ -1,423 +0,0 @@ -/** - * 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 {useEffect} from 'react' -import { - Animated, - Dimensions, - GestureResponderEvent, - GestureResponderHandlers, - NativeTouchEvent, - PanResponder, - PanResponderGestureState, -} from 'react-native' - -import {Position} from '../@types' -import {getImageTranslate} from '../utils' - -const SCREEN = Dimensions.get('window') -const SCREEN_WIDTH = SCREEN.width -const SCREEN_HEIGHT = SCREEN.height -const ANDROID_BAR_HEIGHT = 24 - -const MIN_ZOOM = 2 -const MAX_SCALE = 2 -const DOUBLE_TAP_DELAY = 300 -const OUT_BOUND_MULTIPLIER = 0.75 - -type Props = { - initialScale: number - initialTranslate: Position - onZoom: (isZoomed: boolean) => void -} - -const usePanResponder = ({ - initialScale, - initialTranslate, - onZoom, -}: 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 - - // TODO: It's not valid to reinitialize Animated values during render. - // This is a bug. - 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 getTransformAfterDoubleTap = ( - touchX: number, - touchY: number, - ): [number, Position] => { - let nextScale = initialScale - let nextTranslateX = initialTranslate.x - let nextTranslateY = initialTranslate.y - - // First, let's figure out how much we want to zoom in. - // We want to try to zoom in at least close enough to get rid of black bars. - const imageAspect = imageDimensions.width / imageDimensions.height - const screenAspect = SCREEN.width / SCREEN.height - let zoom = Math.max( - imageAspect / screenAspect, - screenAspect / imageAspect, - MIN_ZOOM, - ) - // Don't zoom so hard that the original image's pixels become blurry. - zoom = Math.min(zoom, MAX_SCALE / initialScale) - nextScale = initialScale * zoom - - // Next, let's see if we need to adjust the scaled image translation. - // Ideally, we want the tapped point to stay under the finger after the scaling. - const dx = SCREEN.width / 2 - touchX - const dy = SCREEN.height / 2 - (touchY - ANDROID_BAR_HEIGHT) - // Before we try to adjust the translation, check how much wiggle room we have. - // We don't want to introduce new black bars or make existing black bars unbalanced. - const [topBound, leftBound, bottomBound, rightBound] = getBounds(nextScale) - if (leftBound > rightBound) { - // Content fills the screen horizontally so we have horizontal wiggle room. - // Try to keep the tapped point under the finger after zoom. - nextTranslateX += dx * zoom - dx - nextTranslateX = Math.min(nextTranslateX, leftBound) - nextTranslateX = Math.max(nextTranslateX, rightBound) - } - if (topBound > bottomBound) { - // Content fills the screen vertically so we have vertical wiggle room. - // Try to keep the tapped point under the finger after zoom. - nextTranslateY += dy * zoom - dy - nextTranslateY = Math.min(nextTranslateY, topBound) - nextTranslateY = Math.max(nextTranslateY, bottomBound) - } - - return [ - nextScale, - { - x: nextTranslateX, - y: nextTranslateY, - }, - ] - } - - 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 panResponder = PanResponder.create({ - onStartShouldSetPanResponder: () => true, - onStartShouldSetPanResponderCapture: () => true, - onMoveShouldSetPanResponder: () => true, - onMoveShouldSetPanResponderCapture: () => true, - onPanResponderGrant: ( - _: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - numberInitialTouches = gestureState.numberActiveTouches - - if (gestureState.numberActiveTouches > 1) { - return - } - }, - onPanResponderStart: ( - event: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - initialTouches = event.nativeEvent.touches - numberInitialTouches = gestureState.numberActiveTouches - - if (gestureState.numberActiveTouches > 1) { - return - } - - const tapTS = Date.now() - // Handle double tap event by calculating diff between first and second taps timestamps - - isDoubleTapPerformed = Boolean( - lastTapTS && tapTS - lastTapTS < DOUBLE_TAP_DELAY, - ) - - if (isDoubleTapPerformed) { - let nextScale = initialScale - let nextTranslate = initialTranslate - - const willZoom = currentScale === initialScale - if (willZoom) { - const {pageX: touchX, pageY: touchY} = event.nativeEvent.touches[0] - ;[nextScale, nextTranslate] = getTransformAfterDoubleTap( - touchX, - touchY, - ) - } - onZoom(willZoom) - - 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 - } else { - lastTapTS = Date.now() - } - }, - onPanResponderMove: ( - event: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - // Don't need to handle move because double tap in progress (was handled in onStart) - if (isDoubleTapPerformed) { - 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) { - 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} - } - }, - onPanResponderRelease: () => { - if (isDoubleTapPerformed) { - isDoubleTapPerformed = false - } - - if (tmpScale > 0) { - if (tmpScale < initialScale || tmpScale > MAX_SCALE) { - tmpScale = tmpScale < initialScale ? initialScale : MAX_SCALE - 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 - } - }, - onPanResponderTerminationRequest: () => false, - onShouldBlockNativeResponder: () => false, - }) - - return [panResponder.panHandlers, scaleValue, translateValue] -} - -const getImageDimensionsByTranslate = ( - translate: Position, - screen: {width: number; height: number}, -): {width: number; height: number} => ({ - width: screen.width - translate.x * 2, - height: screen.height - translate.y * 2, -}) - -const getDistanceBetweenTouches = (touches: NativeTouchEvent[]): number => { - const [a, b] = touches - - if (a == null || b == null) { - return 0 - } - - return Math.sqrt( - Math.pow(a.pageX - b.pageX, 2) + Math.pow(a.pageY - b.pageY, 2), - ) -} - -export default usePanResponder |