diff options
author | dan <dan.abramov@gmail.com> | 2024-11-06 00:21:35 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-11-06 00:21:35 +0000 |
commit | 206df2ab801d211a412f9ce3694d90bdd053caaa (patch) | |
tree | e185c0694eba262c48bd08fc0f430dd0fb203a47 /src/view/com/lightbox/ImageViewing/components | |
parent | 6b826fb88dd33fe594fd7bb631a90d1a1713d0df (diff) | |
download | voidsky-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')
3 files changed, 110 insertions, 83 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 487acf931..ea77ec273 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx @@ -1,7 +1,9 @@ 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, + measure, runOnJS, useAnimatedReaction, useAnimatedRef, @@ -24,13 +26,6 @@ import { TransformMatrix, } from '../../transforms' -const windowDim = Dimensions.get('window') -const screenDim = Dimensions.get('screen') -const statusBarHeight = windowDim.height - screenDim.height -const SCREEN = { - width: windowDim.width, - height: windowDim.height + statusBarHeight, -} const MIN_DOUBLE_TAP_SCALE = 2 const MAX_ORIGINAL_IMAGE_ZOOM = 2 @@ -43,6 +38,7 @@ type Props = { onZoom: (isZoomed: boolean) => void isScrollViewBeingDragged: boolean showControls: boolean + safeAreaRef: AnimatedRef<View> } const ImageItem = ({ imageSrc, @@ -50,6 +46,7 @@ const ImageItem = ({ onZoom, onRequestClose, isScrollViewBeingDragged, + safeAreaRef, }: Props) => { const [isScaled, setIsScaled] = useState(false) const [imageAspect, imageDimensions] = useImageDimensions({ @@ -102,10 +99,10 @@ const ImageItem = ({ const [translateX, translateY, scale] = readTransform(t) const dismissDistance = dismissSwipeTranslateY.value - const dismissProgress = Math.min( - Math.abs(dismissDistance) / (SCREEN.height / 2), - 1, - ) + const screenSize = measure(safeAreaRef) + const dismissProgress = screenSize + ? Math.min(Math.abs(dismissDistance) / (screenSize.height / 2), 1) + : 0 return { opacity: 1 - dismissProgress, transform: [ @@ -120,6 +117,7 @@ const ImageItem = ({ // If the user tried to pan too hard, this function will provide the negative panning to stay in bounds. function getExtraTranslationToStayInBounds( candidateTransform: TransformMatrix, + screenSize: {width: number; height: number}, ) { 'worklet' if (!imageAspect) { @@ -127,16 +125,20 @@ const ImageItem = ({ } const [nextTranslateX, nextTranslateY, nextScale] = readTransform(candidateTransform) - const scaledDimensions = getScaledDimensions(imageAspect, nextScale) + const scaledDimensions = getScaledDimensions( + imageAspect, + nextScale, + screenSize, + ) const clampedTranslateX = clampTranslation( nextTranslateX, scaledDimensions.width, - SCREEN.width, + screenSize.width, ) const clampedTranslateY = clampTranslation( nextTranslateY, scaledDimensions.height, - SCREEN.height, + screenSize.height, ) const dx = clampedTranslateX - nextTranslateX const dy = clampedTranslateY - nextTranslateY @@ -146,21 +148,26 @@ const ImageItem = ({ const pinch = Gesture.Pinch() .onStart(e => { 'worklet' + const screenSize = measure(safeAreaRef) + if (!screenSize) { + return + } pinchOrigin.value = { - x: e.focalX - SCREEN.width / 2, - y: e.focalY - SCREEN.height / 2, + x: e.focalX - screenSize.width / 2, + y: e.focalY - screenSize.height / 2, } }) .onChange(e => { 'worklet' - if (!imageDimensions) { + const screenSize = measure(safeAreaRef) + if (!imageDimensions || !screenSize) { return } // Don't let the picture zoom in so close that it gets blurry. // Also, like in stock Android apps, don't let the user zoom out further than 1:1. const [, , committedScale] = readTransform(committedTransform.value) const maxCommittedScale = - (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM + (imageDimensions.width / screenSize.width) * MAX_ORIGINAL_IMAGE_ZOOM const minPinchScale = 1 / committedScale const maxPinchScale = maxCommittedScale / committedScale const nextPinchScale = Math.min( @@ -175,7 +182,7 @@ const ImageItem = ({ prependPan(t, panTranslation.value) prependPinch(t, nextPinchScale, pinchOrigin.value, pinchTranslation.value) prependTransform(t, committedTransform.value) - const [dx, dy] = getExtraTranslationToStayInBounds(t) + const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize) if (dx !== 0 || dy !== 0) { pinchTranslation.value = { x: pinchTranslation.value.x + dx, @@ -209,9 +216,11 @@ const ImageItem = ({ .minPointers(isScaled ? 1 : 2) .onChange(e => { 'worklet' - if (!imageDimensions) { + const screenSize = measure(safeAreaRef) + if (!imageDimensions || !screenSize) { return } + const nextPanTranslation = {x: e.translationX, y: e.translationY} let t = createTransform() prependPan(t, nextPanTranslation) @@ -224,7 +233,7 @@ const ImageItem = ({ prependTransform(t, committedTransform.value) // Prevent panning from going out of bounds. - const [dx, dy] = getExtraTranslationToStayInBounds(t) + const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize) nextPanTranslation.x += dx nextPanTranslation.y += dy panTranslation.value = nextPanTranslation @@ -251,7 +260,8 @@ const ImageItem = ({ .numberOfTaps(2) .onEnd(e => { 'worklet' - if (!imageDimensions || !imageAspect) { + const screenSize = measure(safeAreaRef) + if (!imageDimensions || !imageAspect || !screenSize) { return } const [, , committedScale] = readTransform(committedTransform.value) @@ -263,7 +273,7 @@ const ImageItem = ({ } // Try to zoom in so that we get rid of the black bars (whatever the orientation was). - const screenAspect = SCREEN.width / SCREEN.height + const screenAspect = screenSize.width / screenSize.height const candidateScale = Math.max( imageAspect / screenAspect, screenAspect / imageAspect, @@ -271,20 +281,23 @@ const ImageItem = ({ ) // But don't zoom in so close that the picture gets blurry. const maxScale = - (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM + (imageDimensions.width / screenSize.width) * MAX_ORIGINAL_IMAGE_ZOOM const scale = Math.min(candidateScale, maxScale) // Calculate where we would be if the user pinched into the double tapped point. // We won't use this transform directly because it may go out of bounds. const candidateTransform = createTransform() const origin = { - x: e.absoluteX - SCREEN.width / 2, - y: e.absoluteY - SCREEN.height / 2, + x: e.absoluteX - screenSize.width / 2, + y: e.absoluteY - screenSize.height / 2, } prependPinch(candidateTransform, scale, origin, {x: 0, y: 0}) // Now we know how much we went out of bounds, so we can shoot correctly. - const [dx, dy] = getExtraTranslationToStayInBounds(candidateTransform) + const [dx, dy] = getExtraTranslationToStayInBounds( + candidateTransform, + screenSize, + ) const finalTransform = createTransform() prependPinch(finalTransform, scale, origin, {x: dx, y: dy}) committedTransform.value = withClampedSpring(finalTransform) @@ -348,8 +361,7 @@ const ImageItem = ({ const styles = StyleSheet.create({ container: { - width: SCREEN.width, - height: SCREEN.height, + height: '100%', overflow: 'hidden', }, image: { @@ -367,19 +379,20 @@ const styles = StyleSheet.create({ function getScaledDimensions( imageAspect: number, scale: number, + screenSize: {width: number; height: number}, ): ImageDimensions { 'worklet' - const screenAspect = SCREEN.width / SCREEN.height + const screenAspect = screenSize.width / screenSize.height const isLandscape = imageAspect > screenAspect if (isLandscape) { return { - width: scale * SCREEN.width, - height: (scale * SCREEN.width) / imageAspect, + width: scale * screenSize.width, + height: (scale * screenSize.width) / imageAspect, } } else { return { - width: scale * SCREEN.height * imageAspect, - height: scale * SCREEN.height, + width: scale * screenSize.height * imageAspect, + height: scale * screenSize.height, } } } 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 { diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx index 4cb7903ef..383bec995 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx @@ -2,6 +2,7 @@ import React from 'react' import {View} from 'react-native' +import {AnimatedRef} from 'react-native-reanimated' import {ImageSource} from '../../@types' @@ -12,6 +13,7 @@ type Props = { onZoom: (scaled: boolean) => void isScrollViewBeingDragged: boolean showControls: boolean + safeAreaRef: AnimatedRef<View> } const ImageItem = (_props: Props) => { |