about summary refs log tree commit diff
path: root/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx')
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx175
1 files changed, 137 insertions, 38 deletions
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
index 03bf45af1..f379df22f 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
@@ -16,57 +16,45 @@ import {
   View,
   NativeScrollEvent,
   NativeSyntheticEvent,
+  NativeTouchEvent,
   TouchableWithoutFeedback,
 } from 'react-native'
 import {Image} from 'expo-image'
 
-import useDoubleTapToZoom from '../../hooks/useDoubleTapToZoom'
 import useImageDimensions from '../../hooks/useImageDimensions'
 
 import {getImageStyles, getImageTransform} from '../../utils'
 import {ImageSource} from '../../@types'
 import {ImageLoading} from './ImageLoading'
 
+const DOUBLE_TAP_DELAY = 300
 const SWIPE_CLOSE_OFFSET = 75
 const SWIPE_CLOSE_VELOCITY = 1
 const SCREEN = Dimensions.get('screen')
 const SCREEN_WIDTH = SCREEN.width
 const SCREEN_HEIGHT = SCREEN.height
+const MIN_ZOOM = 2
 const MAX_SCALE = 2
 
 type Props = {
   imageSrc: ImageSource
   onRequestClose: () => void
   onZoom: (scaled: boolean) => void
-  onLongPress: (image: ImageSource) => void
-  delayLongPress: number
-  swipeToCloseEnabled?: boolean
-  doubleTapToZoomEnabled?: boolean
 }
 
 const AnimatedImage = Animated.createAnimatedComponent(Image)
 
-const ImageItem = ({
-  imageSrc,
-  onZoom,
-  onRequestClose,
-  onLongPress,
-  delayLongPress,
-  swipeToCloseEnabled = true,
-  doubleTapToZoomEnabled = true,
-}: Props) => {
+let lastTapTS: number | null = null
+
+const ImageItem = ({imageSrc, onZoom, onRequestClose}: Props) => {
   const scrollViewRef = useRef<ScrollView>(null)
   const [loaded, setLoaded] = useState(false)
   const [scaled, setScaled] = useState(false)
   const imageDimensions = useImageDimensions(imageSrc)
-  const handleDoubleTap = useDoubleTapToZoom(
-    scrollViewRef,
-    scaled,
-    SCREEN,
-    imageDimensions,
-  )
-
   const [translate, scale] = getImageTransform(imageDimensions, SCREEN)
+
+  // TODO: It's not valid to reinitialize Animated values during render.
+  // This is a bug.
   const scrollValueY = new Animated.Value(0)
   const scaleValue = new Animated.Value(scale || 1)
   const translateValue = new Animated.ValueXY(translate)
@@ -91,15 +79,11 @@ const ImageItem = ({
       onZoom(currentScaled)
       setScaled(currentScaled)
 
-      if (
-        !currentScaled &&
-        swipeToCloseEnabled &&
-        Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY
-      ) {
+      if (!currentScaled && Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY) {
         onRequestClose()
       }
     },
-    [onRequestClose, onZoom, swipeToCloseEnabled],
+    [onRequestClose, onZoom],
   )
 
   const onScroll = ({nativeEvent}: NativeSyntheticEvent<NativeScrollEvent>) => {
@@ -112,9 +96,40 @@ const ImageItem = ({
     scrollValueY.setValue(offsetY)
   }
 
-  const onLongPressHandler = useCallback(() => {
-    onLongPress(imageSrc)
-  }, [imageSrc, onLongPress])
+  const handleDoubleTap = useCallback(
+    (event: NativeSyntheticEvent<NativeTouchEvent>) => {
+      const nowTS = new Date().getTime()
+      const scrollResponderRef = scrollViewRef?.current?.getScrollResponder()
+
+      if (lastTapTS && nowTS - lastTapTS < DOUBLE_TAP_DELAY) {
+        let nextZoomRect = {
+          x: 0,
+          y: 0,
+          width: SCREEN.width,
+          height: SCREEN.height,
+        }
+
+        const willZoom = !scaled
+        if (willZoom) {
+          const {pageX, pageY} = event.nativeEvent
+          nextZoomRect = getZoomRectAfterDoubleTap(
+            imageDimensions,
+            pageX,
+            pageY,
+          )
+        }
+
+        // @ts-ignore
+        scrollResponderRef?.scrollResponderZoomTo({
+          ...nextZoomRect, // This rect is in screen coordinates
+          animated: true,
+        })
+      } else {
+        lastTapTS = nowTS
+      }
+    },
+    [imageDimensions, scaled],
+  )
 
   return (
     <View>
@@ -126,17 +141,13 @@ const ImageItem = ({
         showsVerticalScrollIndicator={false}
         maximumZoomScale={maxScrollViewZoom}
         contentContainerStyle={styles.imageScrollContainer}
-        scrollEnabled={swipeToCloseEnabled}
+        scrollEnabled={true}
+        onScroll={onScroll}
         onScrollEndDrag={onScrollEndDrag}
-        scrollEventThrottle={1}
-        {...(swipeToCloseEnabled && {
-          onScroll,
-        })}>
+        scrollEventThrottle={1}>
         {(!loaded || !imageDimensions) && <ImageLoading />}
         <TouchableWithoutFeedback
-          onPress={doubleTapToZoomEnabled ? handleDoubleTap : undefined}
-          onLongPress={onLongPressHandler}
-          delayLongPress={delayLongPress}
+          onPress={handleDoubleTap}
           accessibilityRole="image"
           accessibilityLabel={imageSrc.alt}
           accessibilityHint="">
@@ -161,4 +172,92 @@ const styles = StyleSheet.create({
   },
 })
 
+const getZoomRectAfterDoubleTap = (
+  imageDimensions: {width: number; height: number} | null,
+  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,
+  }
+}
+
 export default React.memo(ImageItem)