From d2c253a284b3341e92ae104e49f2584602795575 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 20 Sep 2023 02:32:44 +0100 Subject: Make "double tap to zoom" precise across platforms (#1482) * Implement double tap for Android * Match the new behavior on iOS --- .../lightbox/ImageViewing/hooks/usePanResponder.ts | 100 +++++++++++++-------- 1 file changed, 64 insertions(+), 36 deletions(-) (limited to 'src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts') diff --git a/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts b/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts index 036e7246f..c35b1c3d1 100644 --- a/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts +++ b/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts @@ -29,8 +29,10 @@ 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 ANDROID_BAR_HEIGHT = 24 -const SCALE_MAX = 2 +const MIN_ZOOM = 2 +const MAX_SCALE = 2 const DOUBLE_TAP_DELAY = 300 const OUT_BOUND_MULTIPLIER = 0.75 @@ -87,23 +89,56 @@ const usePanResponder = ({ 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 + 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 (translate.y > topBound) { - inBoundTranslate.y = topBound - } else if (translate.y < bottomBound) { - inBoundTranslate.y = bottomBound + 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 inBoundTranslate + return [ + nextScale, + { + x: nextTranslateX, + y: nextTranslateY, + }, + ] } const fitsScreenByWidth = () => @@ -157,25 +192,18 @@ const usePanResponder = ({ ) if (doubleTapToZoomEnabled && isDoubleTapPerformed) { - 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) + 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( [ @@ -336,8 +364,8 @@ const usePanResponder = ({ } if (tmpScale > 0) { - if (tmpScale < initialScale || tmpScale > SCALE_MAX) { - tmpScale = tmpScale < initialScale ? initialScale : SCALE_MAX + if (tmpScale < initialScale || tmpScale > MAX_SCALE) { + tmpScale = tmpScale < initialScale ? initialScale : MAX_SCALE Animated.timing(scaleValue, { toValue: tmpScale, duration: 100, -- cgit 1.4.1 From 8b8fba72843a7875dbedd3c9add6d2487b818bc5 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 20 Sep 2023 03:46:59 +0100 Subject: Inline createPanResponder (#1483) --- .../lightbox/ImageViewing/hooks/usePanResponder.ts | 25 +++++++------ src/view/com/lightbox/ImageViewing/utils.ts | 43 +--------------------- 2 files changed, 15 insertions(+), 53 deletions(-) (limited to 'src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts') diff --git a/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts b/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts index c35b1c3d1..7908504ea 100644 --- a/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts +++ b/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ /** * Copyright (c) JOB TODAY S.A. and its affiliates. * @@ -7,19 +6,19 @@ * */ -import {useMemo, useEffect} from 'react' +import {useEffect} from 'react' import { Animated, Dimensions, GestureResponderEvent, GestureResponderHandlers, NativeTouchEvent, + PanResponder, PanResponderGestureState, } from 'react-native' import {Position} from '../@types' import { - createPanResponder, getDistanceBetweenTouches, getImageTranslate, getImageDimensionsByTranslate, @@ -160,8 +159,12 @@ const usePanResponder = ({ longPressHandlerRef && clearTimeout(longPressHandlerRef) } - const handlers = { - onGrant: ( + const panResponder = PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onStartShouldSetPanResponderCapture: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + onPanResponderGrant: ( _: GestureResponderEvent, gestureState: PanResponderGestureState, ) => { @@ -173,7 +176,7 @@ const usePanResponder = ({ longPressHandlerRef = setTimeout(onLongPress, delayLongPress) }, - onStart: ( + onPanResponderStart: ( event: GestureResponderEvent, gestureState: PanResponderGestureState, ) => { @@ -234,7 +237,7 @@ const usePanResponder = ({ lastTapTS = Date.now() } }, - onMove: ( + onPanResponderMove: ( event: GestureResponderEvent, gestureState: PanResponderGestureState, ) => { @@ -356,7 +359,7 @@ const usePanResponder = ({ tmpTranslate = {x: nextTranslateX, y: nextTranslateY} } }, - onRelease: () => { + onPanResponderRelease: () => { cancelLongPressHandle() if (isDoubleTapPerformed) { @@ -418,9 +421,9 @@ const usePanResponder = ({ tmpTranslate = null } }, - } - - const panResponder = useMemo(() => createPanResponder(handlers), [handlers]) + onPanResponderTerminationRequest: () => false, + onShouldBlockNativeResponder: () => false, + }) return [panResponder.panHandlers, scaleValue, translateValue] } diff --git a/src/view/com/lightbox/ImageViewing/utils.ts b/src/view/com/lightbox/ImageViewing/utils.ts index 8c9c1b34c..d56eea4f4 100644 --- a/src/view/com/lightbox/ImageViewing/utils.ts +++ b/src/view/com/lightbox/ImageViewing/utils.ts @@ -6,14 +6,7 @@ * */ -import { - Animated, - GestureResponderEvent, - PanResponder, - PanResponderGestureState, - PanResponderInstance, - NativeTouchEvent, -} from 'react-native' +import {Animated, NativeTouchEvent} from 'react-native' import {Dimensions, Position} from './@types' type CacheStorageItem = {key: string; value: any} @@ -131,40 +124,6 @@ export const getImageTranslateForScale = ( return getImageTranslate(targetImageDimensions, screen) } -type HandlerType = ( - event: GestureResponderEvent, - state: PanResponderGestureState, -) => void - -type PanResponderProps = { - onGrant: HandlerType - onStart?: HandlerType - onMove: HandlerType - onRelease?: HandlerType - onTerminate?: HandlerType -} - -export const createPanResponder = ({ - onGrant, - onStart, - onMove, - onRelease, - onTerminate, -}: PanResponderProps): PanResponderInstance => - PanResponder.create({ - onStartShouldSetPanResponder: () => true, - onStartShouldSetPanResponderCapture: () => true, - onMoveShouldSetPanResponder: () => true, - onMoveShouldSetPanResponderCapture: () => true, - onPanResponderGrant: onGrant, - onPanResponderStart: onStart, - onPanResponderMove: onMove, - onPanResponderRelease: onRelease, - onPanResponderTerminate: onTerminate, - onPanResponderTerminationRequest: () => false, - onShouldBlockNativeResponder: () => false, - }) - export const getDistanceBetweenTouches = ( touches: NativeTouchEvent[], ): number => { -- cgit 1.4.1