Make "double tap to zoom" precise across platforms (#1482)
* Implement double tap for Android * Match the new behavior on iOS
This commit is contained in:
@@ -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)
|
||||
const getTransformAfterDoubleTap = (
|
||||
touchX: number,
|
||||
touchY: number,
|
||||
): [number, Position] => {
|
||||
let nextScale = initialScale
|
||||
let nextTranslateX = initialTranslate.x
|
||||
let nextTranslateY = initialTranslate.y
|
||||
|
||||
if (translate.x > leftBound) {
|
||||
inBoundTranslate.x = leftBound
|
||||
} else if (translate.x < rightBound) {
|
||||
inBoundTranslate.x = rightBound
|
||||
// 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)
|
||||
}
|
||||
|
||||
if (translate.y > topBound) {
|
||||
inBoundTranslate.y = topBound
|
||||
} else if (translate.y < bottomBound) {
|
||||
inBoundTranslate.y = 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,
|
||||
)
|
||||
let nextScale = initialScale
|
||||
let nextTranslate = initialTranslate
|
||||
|
||||
onZoom(!isScaled)
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user