diff options
author | Eric Bailey <git@esb.lol> | 2023-09-20 11:03:57 -0500 |
---|---|---|
committer | Eric Bailey <git@esb.lol> | 2023-09-20 11:03:57 -0500 |
commit | 5665968f729b99509d54769f494bbbfc59b4b630 (patch) | |
tree | bfad6f82b613699ba3f206d460f0eac50dee6bd4 /src/view/com/lightbox/ImageViewing/hooks/useDoubleTapToZoom.ts | |
parent | 63527493fd8dfb72d21bd50cd2404a5cf2c6e274 (diff) | |
parent | cd96f8dcc8692aec4b9b165cc9f47d7e0b6257df (diff) | |
download | voidsky-5665968f729b99509d54769f494bbbfc59b4b630.tar.zst |
Merge remote-tracking branch 'origin' into bnewbold/bump-api-dep
* origin: Allow touch at the top of the lightbox (#1489) Bump ios build number Feeds tab fixes (#1486) Nicer 'post processing status' in the composer (#1472) Inline createPanResponder (#1483) Tree view threads experiment (#1480) Make "double tap to zoom" precise across platforms (#1482) Onboarding recommended follows (#1457) Add thread sort settings (#1475)
Diffstat (limited to 'src/view/com/lightbox/ImageViewing/hooks/useDoubleTapToZoom.ts')
-rw-r--r-- | src/view/com/lightbox/ImageViewing/hooks/useDoubleTapToZoom.ts | 121 |
1 files changed, 103 insertions, 18 deletions
diff --git a/src/view/com/lightbox/ImageViewing/hooks/useDoubleTapToZoom.ts b/src/view/com/lightbox/ImageViewing/hooks/useDoubleTapToZoom.ts index 92746e951..ea81d9f1c 100644 --- a/src/view/com/lightbox/ImageViewing/hooks/useDoubleTapToZoom.ts +++ b/src/view/com/lightbox/ImageViewing/hooks/useDoubleTapToZoom.ts @@ -12,6 +12,8 @@ import {ScrollView, NativeTouchEvent, NativeSyntheticEvent} from 'react-native' import {Dimensions} from '../@types' const DOUBLE_TAP_DELAY = 300 +const MIN_ZOOM = 2 + let lastTapTS: number | null = null /** @@ -22,41 +24,124 @@ function useDoubleTapToZoom( scrollViewRef: React.RefObject<ScrollView>, scaled: boolean, screen: Dimensions, + imageDimensions: Dimensions | null, ) { const handleDoubleTap = useCallback( (event: NativeSyntheticEvent<NativeTouchEvent>) => { const nowTS = new Date().getTime() const scrollResponderRef = scrollViewRef?.current?.getScrollResponder() + const getZoomRectAfterDoubleTap = ( + touchX: number, + touchY: number, + ): { + x: number + y: number + width: number + height: number + } => { + if (!imageDimensions) { + return { + x: 0, + y: 0, + width: screen.width, + height: screen.height, + } + } + + // 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 + const zoom = Math.max( + imageAspect / screenAspect, + screenAspect / imageAspect, + MIN_ZOOM, + ) + // Unlike in the Android version, we don't constrain the *max* zoom level here. + // Instead, this is done in the ScrollView props so that it constraints pinch too. + + // Next, we'll be calculating the rectangle to "zoom into" in screen coordinates. + // We already know the zoom level, so this gives us the rectangle size. + let rectWidth = screen.width / zoom + let rectHeight = screen.height / zoom + + // Before we settle on the zoomed rect, figure out the safe area it has to be inside. + // We don't want to introduce new black bars or make existing black bars unbalanced. + let minX = 0 + let minY = 0 + let maxX = screen.width - rectWidth + let maxY = screen.height - rectHeight + if (imageAspect >= screenAspect) { + // The image has horizontal black bars. Exclude them from the safe area. + const renderedHeight = screen.width / imageAspect + const horizontalBarHeight = (screen.height - renderedHeight) / 2 + minY += horizontalBarHeight + maxY -= horizontalBarHeight + } else { + // The image has vertical black bars. Exclude them from the safe area. + const renderedWidth = screen.height * imageAspect + const verticalBarWidth = (screen.width - renderedWidth) / 2 + minX += verticalBarWidth + maxX -= verticalBarWidth + } + + // Finally, we can position the rect according to its size and the safe area. + let rectX + if (maxX >= minX) { + // Content fills the screen horizontally so we have horizontal wiggle room. + // Try to keep the tapped point under the finger after zoom. + rectX = touchX - touchX / zoom + rectX = Math.min(rectX, maxX) + rectX = Math.max(rectX, minX) + } else { + // Keep the rect centered on the screen so that black bars are balanced. + rectX = screen.width / 2 - rectWidth / 2 + } + let rectY + if (maxY >= minY) { + // Content fills the screen vertically so we have vertical wiggle room. + // Try to keep the tapped point under the finger after zoom. + rectY = touchY - touchY / zoom + rectY = Math.min(rectY, maxY) + rectY = Math.max(rectY, minY) + } else { + // Keep the rect centered on the screen so that black bars are balanced. + rectY = screen.height / 2 - rectHeight / 2 + } + + return { + x: rectX, + y: rectY, + height: rectHeight, + width: rectWidth, + } + } + if (lastTapTS && nowTS - lastTapTS < DOUBLE_TAP_DELAY) { - const {pageX, pageY} = event.nativeEvent - let targetX = 0 - let targetY = 0 - let targetWidth = screen.width - let targetHeight = screen.height - - // Zooming in - // TODO: Add more precise calculation of targetX, targetY based on touch - if (!scaled) { - targetX = pageX / 2 - targetY = pageY / 2 - targetWidth = screen.width / 2 - targetHeight = screen.height / 2 + let nextZoomRect = { + x: 0, + y: 0, + width: screen.width, + height: screen.height, + } + + const willZoom = !scaled + if (willZoom) { + const {pageX, pageY} = event.nativeEvent + nextZoomRect = getZoomRectAfterDoubleTap(pageX, pageY) } // @ts-ignore scrollResponderRef?.scrollResponderZoomTo({ - x: targetX, - y: targetY, - width: targetWidth, - height: targetHeight, + ...nextZoomRect, // This rect is in screen coordinates animated: true, }) } else { lastTapTS = nowTS } }, - [scaled, screen.height, screen.width, scrollViewRef], + [imageDimensions, scaled, screen.height, screen.width, scrollViewRef], ) return handleDoubleTap |