about summary refs log tree commit diff
path: root/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-11-06 00:21:35 +0000
committerGitHub <noreply@github.com>2024-11-06 00:21:35 +0000
commit206df2ab801d211a412f9ce3694d90bdd053caaa (patch)
treee185c0694eba262c48bd08fc0f430dd0fb203a47 /src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
parent6b826fb88dd33fe594fd7bb631a90d1a1713d0df (diff)
downloadvoidsky-206df2ab801d211a412f9ce3694d90bdd053caaa.tar.zst
Remove SCREEN from lightbox layout (#6124)
* Assign an ID to lightbox and use it as a key

* Consolidate lightbox props into an object

* Remove unused prop

* Move SafeAreaView declaration

* Keep SafeAreaView always mounted

When exploring Android animation, I noticed its content jumps on the first frame. I think this should help prevent that.

* Pass safe area down for measurement

* Remove dependency on SCREEN in Android event handlers

* Remove dependency on SCREEN in iOS event handlers

* Remove dependency on SCREEN on iOS

* Remove dependency on SCREEN on Android

* Remove dependency on JS calc in controls

* Use flex for iOS layout
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.tsx108
1 files changed, 60 insertions, 48 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 a96a1c913..e8f36d520 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
@@ -7,15 +7,18 @@
  */
 
 import React, {useState} from 'react'
-import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native'
+import {ActivityIndicator, StyleSheet, View} from 'react-native'
 import {Gesture, GestureDetector} from 'react-native-gesture-handler'
 import Animated, {
+  AnimatedRef,
   interpolate,
+  measure,
   runOnJS,
   useAnimatedRef,
   useAnimatedStyle,
   useSharedValue,
 } from 'react-native-reanimated'
+import {useSafeAreaFrame} from 'react-native-safe-area-context'
 import {Image} from 'expo-image'
 
 import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
@@ -24,7 +27,6 @@ import {ImageSource} from '../../@types'
 
 const SWIPE_CLOSE_OFFSET = 75
 const SWIPE_CLOSE_VELOCITY = 1
-const SCREEN = Dimensions.get('screen')
 const MAX_ORIGINAL_IMAGE_ZOOM = 2
 const MIN_DOUBLE_TAP_SCALE = 2
 
@@ -35,6 +37,7 @@ type Props = {
   onZoom: (scaled: boolean) => void
   isScrollViewBeingDragged: boolean
   showControls: boolean
+  safeAreaRef: AnimatedRef<View>
 }
 
 const ImageItem = ({
@@ -43,20 +46,24 @@ const ImageItem = ({
   onZoom,
   onRequestClose,
   showControls,
+  safeAreaRef,
 }: Props) => {
   const scrollViewRef = useAnimatedRef<Animated.ScrollView>()
   const translationY = useSharedValue(0)
   const [scaled, setScaled] = useState(false)
+  const screenSizeDelayedForJSThreadOnly = useSafeAreaFrame()
   const [imageAspect, imageDimensions] = useImageDimensions({
     src: imageSrc.uri,
     knownDimensions: imageSrc.dimensions,
   })
   const maxZoomScale = imageDimensions
-    ? (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM
+    ? (imageDimensions.width / screenSizeDelayedForJSThreadOnly.width) *
+      MAX_ORIGINAL_IMAGE_ZOOM
     : 1
 
   const animatedStyle = useAnimatedStyle(() => {
     return {
+      flex: 1,
       opacity: interpolate(
         translationY.value,
         [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET],
@@ -90,24 +97,13 @@ const ImageItem = ({
     setScaled(nextIsScaled)
   }
 
-  function handleDoubleTap(absoluteX: number, absoluteY: number) {
+  function zoomTo(nextZoomRect: {
+    x: number
+    y: number
+    width: number
+    height: number
+  }) {
     const scrollResponderRef = scrollViewRef?.current?.getScrollResponder()
-    let nextZoomRect = {
-      x: 0,
-      y: 0,
-      width: SCREEN.width,
-      height: SCREEN.height,
-    }
-
-    const willZoom = !scaled
-    if (willZoom) {
-      nextZoomRect = getZoomRectAfterDoubleTap(
-        imageAspect,
-        absoluteX,
-        absoluteY,
-      )
-    }
-
     // @ts-ignore
     scrollResponderRef?.scrollResponderZoomTo({
       ...nextZoomRect, // This rect is in screen coordinates
@@ -124,8 +120,27 @@ const ImageItem = ({
     .numberOfTaps(2)
     .onEnd(e => {
       'worklet'
+      const screenSize = measure(safeAreaRef)
+      if (!screenSize) {
+        return
+      }
       const {absoluteX, absoluteY} = e
-      runOnJS(handleDoubleTap)(absoluteX, absoluteY)
+      let nextZoomRect = {
+        x: 0,
+        y: 0,
+        width: screenSize.width,
+        height: screenSize.height,
+      }
+      const willZoom = !scaled
+      if (willZoom) {
+        nextZoomRect = getZoomRectAfterDoubleTap(
+          imageAspect,
+          absoluteX,
+          absoluteY,
+          screenSize,
+        )
+      }
+      runOnJS(zoomTo)(nextZoomRect)
     })
 
   const composedGesture = Gesture.Exclusive(doubleTap, singleTap)
@@ -135,13 +150,13 @@ const ImageItem = ({
       <Animated.ScrollView
         // @ts-ignore Something's up with the types here
         ref={scrollViewRef}
-        style={styles.listItem}
         pinchGestureEnabled
         showsHorizontalScrollIndicator={false}
         showsVerticalScrollIndicator={false}
         maximumZoomScale={maxZoomScale}
-        onScroll={scrollHandler}>
-        <Animated.View style={[styles.imageScrollContainer, animatedStyle]}>
+        onScroll={scrollHandler}
+        contentContainerStyle={styles.scrollContainer}>
+        <Animated.View style={animatedStyle}>
           <ActivityIndicator size="small" color="#FFF" style={styles.loading} />
           <Image
             contentFit="contain"
@@ -161,17 +176,6 @@ const ImageItem = ({
 }
 
 const styles = StyleSheet.create({
-  imageScrollContainer: {
-    height: SCREEN.height,
-  },
-  listItem: {
-    width: SCREEN.width,
-    height: SCREEN.height,
-  },
-  image: {
-    width: SCREEN.width,
-    height: SCREEN.height,
-  },
   loading: {
     position: 'absolute',
     top: 0,
@@ -179,30 +183,38 @@ const styles = StyleSheet.create({
     right: 0,
     bottom: 0,
   },
+  scrollContainer: {
+    flex: 1,
+  },
+  image: {
+    flex: 1,
+  },
 })
 
 const getZoomRectAfterDoubleTap = (
   imageAspect: number | undefined,
   touchX: number,
   touchY: number,
+  screenSize: {width: number; height: number},
 ): {
   x: number
   y: number
   width: number
   height: number
 } => {
+  'worklet'
   if (!imageAspect) {
     return {
       x: 0,
       y: 0,
-      width: SCREEN.width,
-      height: SCREEN.height,
+      width: screenSize.width,
+      height: screenSize.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 screenAspect = SCREEN.width / SCREEN.height
+  const screenAspect = screenSize.width / screenSize.height
   const zoom = Math.max(
     imageAspect / screenAspect,
     screenAspect / imageAspect,
@@ -213,25 +225,25 @@ const getZoomRectAfterDoubleTap = (
 
   // 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
+  let rectWidth = screenSize.width / zoom
+  let rectHeight = screenSize.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
+  let maxX = screenSize.width - rectWidth
+  let maxY = screenSize.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
+    const renderedHeight = screenSize.width / imageAspect
+    const horizontalBarHeight = (screenSize.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
+    const renderedWidth = screenSize.height * imageAspect
+    const verticalBarWidth = (screenSize.width - renderedWidth) / 2
     minX += verticalBarWidth
     maxX -= verticalBarWidth
   }
@@ -246,7 +258,7 @@ const getZoomRectAfterDoubleTap = (
     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
+    rectX = screenSize.width / 2 - rectWidth / 2
   }
   let rectY
   if (maxY >= minY) {
@@ -257,7 +269,7 @@ const getZoomRectAfterDoubleTap = (
     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
+    rectY = screenSize.height / 2 - rectHeight / 2
   }
 
   return {