diff options
author | dan <dan.abramov@gmail.com> | 2023-10-05 23:28:56 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-05 15:28:56 -0700 |
commit | 260b03a05c22232373cbf8cb0d7dda41a3302343 (patch) | |
tree | f7ce8b72c80fbdc723245dc34d1db56288b7b176 /src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx | |
parent | eb7306b16512e317f477c7a28e1e3b0ce5c65ff8 (diff) | |
download | voidsky-260b03a05c22232373cbf8cb0d7dda41a3302343.tar.zst |
Remove unused lightbox options (#1616)
* Inline lightbox helpers * Delete unused useImagePrefetch * Delete unused long press gesture * Always enable double tap * Always enable swipe to close * Remove unused onImageIndexChange * Inline custom Hooks into ImageViewing * Declare LightboxFooter outside Lightbox * Add more TODO comments * Inline useDoubleTapToZoom * Remove dead utils, move utils used only once
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.tsx | 175 |
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) |