about summary refs log tree commit diff
path: root/src/view/com/lightbox/ImageViewing/components/ImageItem
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/lightbox/ImageViewing/components/ImageItem')
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx59
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx265
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx5
3 files changed, 127 insertions, 202 deletions
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
index 553a4a2e7..7c7ad0616 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
@@ -1,9 +1,8 @@
-import React, {MutableRefObject, useState} from 'react'
+import React, {useState} from 'react'
 
 import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native'
 import {Image} from 'expo-image'
 import Animated, {
-  measure,
   runOnJS,
   useAnimatedRef,
   useAnimatedStyle,
@@ -12,11 +11,7 @@ import Animated, {
   withDecay,
   withSpring,
 } from 'react-native-reanimated'
-import {
-  GestureDetector,
-  Gesture,
-  GestureType,
-} from 'react-native-gesture-handler'
+import {GestureDetector, Gesture} from 'react-native-gesture-handler'
 import useImageDimensions from '../../hooks/useImageDimensions'
 import {
   createTransform,
@@ -39,16 +34,16 @@ const initialTransform = createTransform()
 type Props = {
   imageSrc: ImageSource
   onRequestClose: () => void
+  onTap: () => void
   onZoom: (isZoomed: boolean) => void
-  pinchGestureRef: MutableRefObject<GestureType | undefined>
   isScrollViewBeingDragged: boolean
 }
 const ImageItem = ({
   imageSrc,
+  onTap,
   onZoom,
   onRequestClose,
   isScrollViewBeingDragged,
-  pinchGestureRef,
 }: Props) => {
   const [isScaled, setIsScaled] = useState(false)
   const [isLoaded, setIsLoaded] = useState(false)
@@ -140,28 +135,7 @@ const ImageItem = ({
     return [dx, dy]
   }
 
-  // This is a hack.
-  // We need to disallow any gestures (and let the native parent scroll view scroll) while you're scrolling it.
-  // However, there is no great reliable way to coordinate this yet in RGNH.
-  // This "fake" manual gesture handler whenever you're trying to touch something while the parent scrollview is not at rest.
-  const consumeHScroll = Gesture.Manual().onTouchesDown((e, manager) => {
-    if (isScrollViewBeingDragged) {
-      // Steal the gesture (and do nothing, so native ScrollView does its thing).
-      manager.activate()
-      return
-    }
-    const measurement = measure(containerRef)
-    if (!measurement || measurement.pageX !== 0) {
-      // Steal the gesture (and do nothing, so native ScrollView does its thing).
-      manager.activate()
-      return
-    }
-    // Fail this "fake" gesture so that the gestures after it can proceed.
-    manager.fail()
-  })
-
   const pinch = Gesture.Pinch()
-    .withRef(pinchGestureRef)
     .onStart(e => {
       pinchOrigin.value = {
         x: e.focalX - SCREEN.width / 2,
@@ -255,6 +229,10 @@ const ImageItem = ({
       panTranslation.value = {x: 0, y: 0}
     })
 
+  const singleTap = Gesture.Tap().onEnd(() => {
+    runOnJS(onTap)()
+  })
+
   const doubleTap = Gesture.Tap()
     .numberOfTaps(2)
     .onEnd(e => {
@@ -318,22 +296,27 @@ const ImageItem = ({
       }
     })
 
+  const composedGesture = isScrollViewBeingDragged
+    ? // If the parent is not at rest, provide a no-op gesture.
+      Gesture.Manual()
+    : Gesture.Exclusive(
+        dismissSwipePan,
+        Gesture.Simultaneous(pinch, pan),
+        doubleTap,
+        singleTap,
+      )
+
   const isLoading = !isLoaded || !imageDimensions
   return (
     <Animated.View ref={containerRef} style={styles.container}>
       {isLoading && (
         <ActivityIndicator size="small" color="#FFF" style={styles.loading} />
       )}
-      <GestureDetector
-        gesture={Gesture.Exclusive(
-          consumeHScroll,
-          dismissSwipePan,
-          Gesture.Simultaneous(pinch, pan),
-          doubleTap,
-        )}>
+      <GestureDetector gesture={composedGesture}>
         <AnimatedImage
-          source={imageSrc}
           contentFit="contain"
+          // NOTE: Don't pass imageSrc={imageSrc} or MobX will break.
+          source={{uri: imageSrc.uri}}
           style={[styles.image, animatedStyle]}
           accessibilityLabel={imageSrc.alt}
           accessibilityHint=""
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 598b18ed2..f73f355ac 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
@@ -6,162 +6,162 @@
  *
  */
 
-import React, {MutableRefObject, useCallback, useRef, useState} from 'react'
+import React, {useState} from 'react'
 
-import {
-  Animated,
-  Dimensions,
-  ScrollView,
-  StyleSheet,
-  View,
-  NativeScrollEvent,
-  NativeSyntheticEvent,
-  NativeTouchEvent,
-  TouchableWithoutFeedback,
-} from 'react-native'
+import {Dimensions, StyleSheet} from 'react-native'
 import {Image} from 'expo-image'
-import {GestureType} from 'react-native-gesture-handler'
+import Animated, {
+  interpolate,
+  runOnJS,
+  useAnimatedRef,
+  useAnimatedScrollHandler,
+  useAnimatedStyle,
+  useSharedValue,
+} from 'react-native-reanimated'
+import {Gesture, GestureDetector} from 'react-native-gesture-handler'
 
 import useImageDimensions from '../../hooks/useImageDimensions'
 
 import {ImageSource, Dimensions as ImageDimensions} 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
+const MAX_ORIGINAL_IMAGE_ZOOM = 2
+const MIN_DOUBLE_TAP_SCALE = 2
 
 type Props = {
   imageSrc: ImageSource
   onRequestClose: () => void
+  onTap: () => void
   onZoom: (scaled: boolean) => void
-  pinchGestureRef: MutableRefObject<GestureType>
   isScrollViewBeingDragged: boolean
 }
 
 const AnimatedImage = Animated.createAnimatedComponent(Image)
 
-let lastTapTS: number | null = null
-
-const ImageItem = ({imageSrc, onZoom, onRequestClose}: Props) => {
-  const scrollViewRef = useRef<ScrollView>(null)
+const ImageItem = ({imageSrc, onTap, onZoom, onRequestClose}: Props) => {
+  const scrollViewRef = useAnimatedRef<Animated.ScrollView>()
+  const translationY = useSharedValue(0)
   const [loaded, setLoaded] = useState(false)
   const [scaled, setScaled] = useState(false)
   const imageDimensions = useImageDimensions(imageSrc)
-  const [translate, scale] = getImageTransform(imageDimensions, SCREEN)
-  const [scrollValueY] = useState(() => new Animated.Value(0))
-  const maxScrollViewZoom = MAX_SCALE / (scale || 1)
+  const maxZoomScale = imageDimensions
+    ? (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM
+    : 1
 
-  const imageOpacity = scrollValueY.interpolate({
-    inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET],
-    outputRange: [0.5, 1, 0.5],
+  const animatedStyle = useAnimatedStyle(() => {
+    return {
+      opacity: interpolate(
+        translationY.value,
+        [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET],
+        [0.5, 1, 0.5],
+      ),
+    }
   })
-  const imagesStyles = getImageStyles(imageDimensions, translate, scale || 1)
-  const imageStylesWithOpacity = {...imagesStyles, opacity: imageOpacity}
 
-  const onScrollEndDrag = useCallback(
-    ({nativeEvent}: NativeSyntheticEvent<NativeScrollEvent>) => {
-      const velocityY = nativeEvent?.velocity?.y ?? 0
-      const currentScaled = nativeEvent?.zoomScale > 1
-
-      onZoom(currentScaled)
-      setScaled(currentScaled)
-
-      if (!currentScaled && Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY) {
-        onRequestClose()
+  const scrollHandler = useAnimatedScrollHandler({
+    onScroll(e) {
+      const nextIsScaled = e.zoomScale > 1
+      translationY.value = nextIsScaled ? 0 : e.contentOffset.y
+      if (scaled !== nextIsScaled) {
+        runOnJS(handleZoom)(nextIsScaled)
       }
     },
-    [onRequestClose, onZoom],
-  )
+    onEndDrag(e) {
+      const velocityY = e.velocity?.y ?? 0
+      const nextIsScaled = e.zoomScale > 1
+      if (scaled !== nextIsScaled) {
+        runOnJS(handleZoom)(nextIsScaled)
+      }
+      if (!nextIsScaled && Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY) {
+        runOnJS(onRequestClose)()
+      }
+    },
+  })
+
+  function handleZoom(nextIsScaled: boolean) {
+    onZoom(nextIsScaled)
+    setScaled(nextIsScaled)
+  }
 
-  const onScroll = ({nativeEvent}: NativeSyntheticEvent<NativeScrollEvent>) => {
-    const offsetY = nativeEvent?.contentOffset?.y ?? 0
+  function handleDoubleTap(absoluteX: number, absoluteY: number) {
+    const scrollResponderRef = scrollViewRef?.current?.getScrollResponder()
+    let nextZoomRect = {
+      x: 0,
+      y: 0,
+      width: SCREEN.width,
+      height: SCREEN.height,
+    }
 
-    if (nativeEvent?.zoomScale > 1) {
-      return
+    const willZoom = !scaled
+    if (willZoom) {
+      nextZoomRect = getZoomRectAfterDoubleTap(
+        imageDimensions,
+        absoluteX,
+        absoluteY,
+      )
     }
 
-    scrollValueY.setValue(offsetY)
+    // @ts-ignore
+    scrollResponderRef?.scrollResponderZoomTo({
+      ...nextZoomRect, // This rect is in screen coordinates
+      animated: true,
+    })
   }
 
-  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 singleTap = Gesture.Tap().onEnd(() => {
+    runOnJS(onTap)()
+  })
 
-        const willZoom = !scaled
-        if (willZoom) {
-          const {pageX, pageY} = event.nativeEvent
-          nextZoomRect = getZoomRectAfterDoubleTap(
-            imageDimensions,
-            pageX,
-            pageY,
-          )
-        }
+  const doubleTap = Gesture.Tap()
+    .numberOfTaps(2)
+    .onEnd(e => {
+      const {absoluteX, absoluteY} = e
+      runOnJS(handleDoubleTap)(absoluteX, absoluteY)
+    })
 
-        // @ts-ignore
-        scrollResponderRef?.scrollResponderZoomTo({
-          ...nextZoomRect, // This rect is in screen coordinates
-          animated: true,
-        })
-      } else {
-        lastTapTS = nowTS
-      }
-    },
-    [imageDimensions, scaled],
-  )
+  const composedGesture = Gesture.Exclusive(doubleTap, singleTap)
 
   return (
-    <View>
-      <ScrollView
+    <GestureDetector gesture={composedGesture}>
+      <Animated.ScrollView
+        // @ts-ignore Something's up with the types here
         ref={scrollViewRef}
         style={styles.listItem}
         pinchGestureEnabled
         showsHorizontalScrollIndicator={false}
         showsVerticalScrollIndicator={false}
-        maximumZoomScale={maxScrollViewZoom}
+        maximumZoomScale={maxZoomScale}
         contentContainerStyle={styles.imageScrollContainer}
-        scrollEnabled={true}
-        onScroll={onScroll}
-        onScrollEndDrag={onScrollEndDrag}
-        scrollEventThrottle={1}>
+        onScroll={scrollHandler}>
         {(!loaded || !imageDimensions) && <ImageLoading />}
-        <TouchableWithoutFeedback
-          onPress={handleDoubleTap}
-          accessibilityRole="image"
+        <AnimatedImage
+          contentFit="contain"
+          // NOTE: Don't pass imageSrc={imageSrc} or MobX will break.
+          source={{uri: imageSrc.uri}}
+          style={[styles.image, animatedStyle]}
           accessibilityLabel={imageSrc.alt}
-          accessibilityHint="">
-          <AnimatedImage
-            source={imageSrc}
-            style={imageStylesWithOpacity}
-            onLoad={() => setLoaded(true)}
-          />
-        </TouchableWithoutFeedback>
-      </ScrollView>
-    </View>
+          accessibilityHint=""
+          onLoad={() => setLoaded(true)}
+        />
+      </Animated.ScrollView>
+    </GestureDetector>
   )
 }
 
 const styles = StyleSheet.create({
+  imageScrollContainer: {
+    height: SCREEN.height,
+  },
   listItem: {
-    width: SCREEN_WIDTH,
-    height: SCREEN_HEIGHT,
+    width: SCREEN.width,
+    height: SCREEN.height,
   },
-  imageScrollContainer: {
-    height: SCREEN_HEIGHT,
+  image: {
+    width: SCREEN.width,
+    height: SCREEN.height,
   },
 })
 
@@ -191,7 +191,7 @@ const getZoomRectAfterDoubleTap = (
   const zoom = Math.max(
     imageAspect / screenAspect,
     screenAspect / imageAspect,
-    MIN_ZOOM,
+    MIN_DOUBLE_TAP_SCALE,
   )
   // 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.
@@ -253,61 +253,4 @@ const getZoomRectAfterDoubleTap = (
   }
 }
 
-const getImageStyles = (
-  image: ImageDimensions | null,
-  translate: {readonly x: number; readonly y: number} | undefined,
-  scale?: number,
-) => {
-  if (!image?.width || !image?.height) {
-    return {width: 0, height: 0}
-  }
-  const transform = []
-  if (translate) {
-    transform.push({translateX: translate.x})
-    transform.push({translateY: translate.y})
-  }
-  if (scale) {
-    // @ts-ignore TODO - is scale incorrect? might need to remove -prf
-    transform.push({scale}, {perspective: new Animated.Value(1000)})
-  }
-  return {
-    width: image.width,
-    height: image.height,
-    transform,
-  }
-}
-
-const getImageTransform = (
-  image: ImageDimensions | null,
-  screen: ImageDimensions,
-) => {
-  if (!image?.width || !image?.height) {
-    return [] as const
-  }
-
-  const wScale = screen.width / image.width
-  const hScale = screen.height / image.height
-  const scale = Math.min(wScale, hScale)
-  const {x, y} = getImageTranslate(image, screen)
-
-  return [{x, y}, scale] as const
-}
-
-const getImageTranslate = (
-  image: ImageDimensions,
-  screen: ImageDimensions,
-): {x: number; y: number} => {
-  const getTranslateForAxis = (axis: 'x' | 'y'): number => {
-    const imageSize = axis === 'x' ? image.width : image.height
-    const screenSize = axis === 'x' ? screen.width : screen.height
-
-    return (screenSize - imageSize) / 2
-  }
-
-  return {
-    x: getTranslateForAxis('x'),
-    y: getTranslateForAxis('y'),
-  }
-}
-
 export default React.memo(ImageItem)
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx
index 898b00c78..16688b820 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx
@@ -1,15 +1,14 @@
 // default implementation fallback for web
 
-import React, {MutableRefObject} from 'react'
+import React from 'react'
 import {View} from 'react-native'
-import {GestureType} from 'react-native-gesture-handler'
 import {ImageSource} from '../../@types'
 
 type Props = {
   imageSrc: ImageSource
   onRequestClose: () => void
+  onTap: () => void
   onZoom: (scaled: boolean) => void
-  pinchGestureRef: MutableRefObject<GestureType | undefined>
   isScrollViewBeingDragged: boolean
 }