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 ed1e81f684..fa433b4af7 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx @@ -1,5 +1,10 @@ import React, {useState} from 'react' -import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native' +import { + ActivityIndicator, + Dimensions, + StyleProp, + StyleSheet, +} from 'react-native' import { Gesture, GestureDetector, @@ -13,7 +18,7 @@ import Animated, { useSharedValue, withSpring, } from 'react-native-reanimated' -import {Image} from 'expo-image' +import {Image, ImageStyle} from 'expo-image' import {useImageDimensions} from '#/lib/media/image-sizes' import type {Dimensions as ImageDimensions, ImageSource} from '../../@types' @@ -27,6 +32,8 @@ import { TransformMatrix, } from '../../transforms' +const AnimatedImage = Animated.createAnimatedComponent(Image) + const windowDim = Dimensions.get('window') const screenDim = Dimensions.get('screen') const statusBarHeight = windowDim.height - screenDim.height @@ -47,13 +54,16 @@ type Props = { isPagingAndroid: boolean showControls: boolean dismissSwipePan: PanGesture + imageStyle: StyleProp } + const ImageItem = ({ imageSrc, onTap, onZoom, isPagingAndroid, dismissSwipePan, + imageStyle, }: Props) => { const [isScaled, setIsScaled] = useState(false) const [imageAspect, imageDimensions] = useImageDimensions({ @@ -307,21 +317,24 @@ const ImageItem = ({ renderToHardwareTextureAndroid style={[styles.container, animatedStyle, {}]}> - } const ImageItem = ({ @@ -40,6 +48,7 @@ const ImageItem = ({ onZoom, showControls, dismissSwipePan, + imageStyle, }: Props) => { const scrollViewRef = useAnimatedRef() @@ -145,21 +154,24 @@ const ImageItem = ({ bounces={scaled} centerContent> - } const ImageItem = (_props: Props) => { diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx index 714418e0af..e832fc7547 100644 --- a/src/view/com/lightbox/ImageViewing/index.tsx +++ b/src/view/com/lightbox/ImageViewing/index.tsx @@ -12,6 +12,7 @@ import React, {useCallback, useMemo, useState} from 'react' import { Dimensions, LayoutAnimation, + PixelRatio, Platform, StyleSheet, View, @@ -23,6 +24,7 @@ import { interpolate, SharedValue, useAnimatedReaction, + useDerivedValue, } from 'react-native-reanimated' import Animated, { runOnJS, @@ -48,7 +50,7 @@ import ImageDefaultHeader from './components/ImageDefaultHeader' import ImageItem from './components/ImageItem/ImageItem' const SCREEN = Dimensions.get('screen') - +const PIXEL_RATIO = PixelRatio.get() const SCREEN_HEIGHT = Dimensions.get('window').height function ImageViewing({ @@ -106,12 +108,7 @@ function ImageViewing({ return {pointerEvents: 'auto'} }) - const activeImageStyle = useAnimatedStyle(() => { - if (openProgress.value === 1 || dismissSwipeTranslateY.value !== 0) { - return { - transform: [{translateY: dismissSwipeTranslateY.value}], - } - } + const activeInterpolation = useDerivedValue(() => { const image = images[imageIndex] if (image.thumbRect && image.dimensions) { const interpolatedTransform = interpolateTransform( @@ -120,8 +117,20 @@ function ImageViewing({ SCREEN, image.dimensions, ) + return interpolatedTransform + } + }) + + const activeImageContainerStyle = useAnimatedStyle(() => { + if (openProgress.value === 1 || dismissSwipeTranslateY.value !== 0) { return { - transform: interpolatedTransform, + transform: [{translateY: dismissSwipeTranslateY.value}], + } + } + const interpolation = activeInterpolation.value + if (interpolation) { + return { + transform: interpolation.transform, } } return { @@ -129,6 +138,17 @@ function ImageViewing({ } }) + const activeImageStyle = useAnimatedStyle(() => { + const interpolation = activeInterpolation.value + if (interpolation) { + return { + height: interpolation.height, + width: interpolation.width, + } + } + return {} + }) + const dismissSwipePan = Gesture.Pan() .enabled(!isScaled) .activeOffsetY([-10, 10]) @@ -230,7 +250,7 @@ function ImageViewing({ {images.map((imageSrc, i) => ( + style={imageIndex === i ? activeImageContainerStyle : null}> ))} @@ -337,6 +358,16 @@ function LightboxFooter({ ) } +function interpolatePx( + px: number, + inputRange: readonly number[], + outputRange: readonly number[], +) { + 'worklet' + const value = interpolate(px, inputRange, outputRange) + return Math.round(value * PIXEL_RATIO) / PIXEL_RATIO +} + function interpolateTransform( progress: number, thumbnailDims: { @@ -349,34 +380,55 @@ function interpolateTransform( imageDims: {width: number; height: number}, ) { 'worklet' + const imageAspect = imageDims.width / imageDims.height + const thumbAspect = thumbnailDims.width / thumbnailDims.height + let uncroppedInitialWidth + let uncroppedInitialHeight + if (imageAspect > thumbAspect) { + uncroppedInitialWidth = thumbnailDims.height * imageAspect + uncroppedInitialHeight = thumbnailDims.height + } else { + uncroppedInitialWidth = thumbnailDims.width + uncroppedInitialHeight = thumbnailDims.width / imageAspect + } + const finalWidth = screenSize.width + const finalHeight = screenSize.width / imageAspect + const initialScale = Math.min( + uncroppedInitialWidth / finalWidth, + uncroppedInitialHeight / finalHeight, + ) + const croppedFinalWidth = thumbnailDims.width / initialScale + const croppedFinalHeight = thumbnailDims.height / initialScale const screenCenterX = screenSize.width / 2 const screenCenterY = screenSize.height / 2 const thumbnailCenterX = thumbnailDims.pageX + thumbnailDims.width / 2 const thumbnailCenterY = thumbnailDims.pageY + thumbnailDims.height / 2 const initialTranslateX = thumbnailCenterX - screenCenterX const initialTranslateY = thumbnailCenterY - screenCenterY - - const imageAspect = imageDims.width / imageDims.height - const thumbAspect = thumbnailDims.width / thumbnailDims.height - const initialWidth = - imageAspect > thumbAspect - ? thumbnailDims.height * imageAspect - : thumbnailDims.width - const initialHeight = - imageAspect > thumbAspect - ? thumbnailDims.height - : thumbnailDims.width / imageAspect - const finalWidth = screenSize.width - const finalHeight = screenSize.width / imageAspect - const initialScale = Math.min( - initialWidth / finalWidth, - initialHeight / finalHeight, - ) - - const translateX = interpolate(progress, [0, 1], [initialTranslateX, 0]) - const translateY = interpolate(progress, [0, 1], [initialTranslateY, 0]) const scale = interpolate(progress, [0, 1], [initialScale, 1]) - return [{translateX}, {translateY}, {scale}] + const translateX = interpolatePx(progress, [0, 1], [initialTranslateX, 0]) + const translateY = interpolatePx(progress, [0, 1], [initialTranslateY, 0]) + const width = interpolatePx(progress, [0, 1], [croppedFinalWidth, finalWidth]) + const height = interpolatePx( + progress, + [0, 1], + [croppedFinalHeight, finalHeight], + ) + const cropTranslateX = interpolatePx( + progress, + [0, 1], + [(finalWidth - croppedFinalWidth) / 2, 0], + ) + return { + transform: [ + {translateX}, + {translateY}, + {scale}, + {translateX: cropTranslateX}, + ], + width, + height, + } } const styles = StyleSheet.create({ @@ -501,7 +553,7 @@ function ImageViewingRoot({ function withClampedSpring(value: any) { 'worklet' - return withSpring(value, {overshootClamping: true, stiffness: 150}) + return withSpring(value, {overshootClamping: true, stiffness: 120}) } export default ImageViewingRoot