Make it fast by animating only affine transforms

This commit is contained in:
Dan Abramov
2024-11-08 02:48:59 +00:00
parent 8569910b2d
commit d78d65e290
5 changed files with 264 additions and 166 deletions
@@ -6,6 +6,7 @@
* *
*/ */
import {TransformsStyle} from 'react-native'
import {MeasuredDimensions} from 'react-native-reanimated' import {MeasuredDimensions} from 'react-native-reanimated'
export type Dimensions = { export type Dimensions = {
@@ -26,3 +27,8 @@ export type ImageSource = {
dimensions: Dimensions | null dimensions: Dimensions | null
type: 'image' | 'circle-avi' | 'rect-avi' type: 'image' | 'circle-avi' | 'rect-avi'
} }
export type Transform = Exclude<
TransformsStyle['transform'],
string | undefined
>
@@ -1,22 +1,26 @@
import React, {useState} from 'react' import React, {useState} from 'react'
import {ActivityIndicator, StyleProp, StyleSheet} from 'react-native' import {ActivityIndicator, StyleSheet} from 'react-native'
import { import {
Gesture, Gesture,
GestureDetector, GestureDetector,
PanGesture, PanGesture,
} from 'react-native-gesture-handler' } from 'react-native-gesture-handler'
import Animated, { import Animated, {
BaseAnimationBuilder,
runOnJS, runOnJS,
SharedValue,
useAnimatedReaction, useAnimatedReaction,
useAnimatedRef, useAnimatedRef,
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
withSpring, withSpring,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {Image, ImageStyle} from 'expo-image' import {Image} from 'expo-image'
import type {Dimensions as ImageDimensions, ImageSource} from '../../@types' import type {
Dimensions as ImageDimensions,
ImageSource,
Transform,
} from '../../@types'
import { import {
applyRounding, applyRounding,
createTransform, createTransform,
@@ -49,9 +53,15 @@ type Props = {
} }
imageAspect: number | undefined imageAspect: number | undefined
imageDimensions: ImageDimensions | undefined imageDimensions: ImageDimensions | undefined
imageStyle: StyleProp<ImageStyle>
dismissSwipePan: PanGesture dismissSwipePan: PanGesture
layoutAnimationAndroid: BaseAnimationBuilder transforms: Readonly<
SharedValue<{
scaleAndMoveTransform: Transform
cropFrameTransform: Transform
cropContentTransform: Transform
isResting: boolean
}>
>
} }
const ImageItem = ({ const ImageItem = ({
imageSrc, imageSrc,
@@ -61,9 +71,8 @@ const ImageItem = ({
measureSafeArea, measureSafeArea,
imageAspect, imageAspect,
imageDimensions, imageDimensions,
imageStyle,
dismissSwipePan, dismissSwipePan,
layoutAnimationAndroid, transforms,
}: Props) => { }: Props) => {
const [isScaled, setIsScaled] = useState(false) const [isScaled, setIsScaled] = useState(false)
const committedTransform = useSharedValue(initialTransform) const committedTransform = useSharedValue(initialTransform)
@@ -101,19 +110,6 @@ const ImageItem = ({
onZoom(nextIsScaled) onZoom(nextIsScaled)
} }
const animatedStyle = useAnimatedStyle(() => {
// Apply the active adjustments on top of the committed transform before the gestures.
// This is matrix multiplication, so operations are applied in the reverse order.
let t = createTransform()
prependPan(t, panTranslation.value)
prependPinch(t, pinchScale.value, pinchOrigin.value, pinchTranslation.value)
prependTransform(t, committedTransform.value)
const [translateX, translateY, scale] = readTransform(t)
return {
transform: [{translateX}, {translateY: translateY}, {scale}],
}
})
// On Android, stock apps prevent going "out of bounds" on pan or pinch. You should "bump" into edges. // On Android, stock apps prevent going "out of bounds" on pan or pinch. You should "bump" into edges.
// If the user tried to pan too hard, this function will provide the negative panning to stay in bounds. // If the user tried to pan too hard, this function will provide the negative panning to stay in bounds.
function getExtraTranslationToStayInBounds( function getExtraTranslationToStayInBounds(
@@ -315,26 +311,92 @@ const ImageItem = ({
singleTap, singleTap,
) )
const containerStyle = useAnimatedStyle(() => {
const {scaleAndMoveTransform} = transforms.value
// Apply the active adjustments on top of the committed transform before the gestures.
// This is matrix multiplication, so operations are applied in the reverse order.
let t = createTransform()
prependPan(t, panTranslation.value)
prependPinch(t, pinchScale.value, pinchOrigin.value, pinchTranslation.value)
prependTransform(t, committedTransform.value)
const [translateX, translateY, scale] = readTransform(t)
const manipulationTransform = [
{translateX},
{translateY: translateY},
{scale},
]
return {
width: '100%',
aspectRatio: imageAspect,
transform: scaleAndMoveTransform.concat(manipulationTransform),
}
})
const imageCropStyle = useAnimatedStyle(() => {
const {cropFrameTransform} = transforms.value
return {
flex: 1,
overflow: 'hidden',
transform: cropFrameTransform,
}
})
const type = imageSrc.type
const borderRadius =
type === 'circle-avi' ? 1e5 : type === 'rect-avi' ? 20 : 0
const imageStyle = useAnimatedStyle(() => {
const {cropContentTransform} = transforms.value
return {
flex: 1,
borderRadius,
transform: cropContentTransform,
}
})
const [showLoader, setShowLoader] = useState(false)
const [hasLoaded, setHasLoaded] = useState(false)
useAnimatedReaction(
() => {
return transforms.value.isResting && !hasLoaded
},
(show, prevShow) => {
if (show && !prevShow) {
runOnJS(setShowLoader)(false)
} else if (!prevShow && show) {
runOnJS(setShowLoader)(true)
}
},
)
return ( return (
<GestureDetector gesture={composedGesture}> <GestureDetector gesture={composedGesture}>
<Animated.View <Animated.View
ref={containerRef} ref={containerRef}
// Necessary to make opacity work for both children together. style={[styles.container]}
renderToHardwareTextureAndroid renderToHardwareTextureAndroid>
style={[styles.container, animatedStyle]}> <Animated.View style={containerStyle}>
<ActivityIndicator size="small" color="#FFF" style={styles.loading} /> {showLoader && (
<AnimatedImage <ActivityIndicator
contentFit="cover" size="small"
source={{uri: imageSrc.uri}} color="#FFF"
placeholderContentFit="cover" style={styles.loading}
placeholder={{uri: imageSrc.thumbUri}} />
style={imageStyle} )}
layout={layoutAnimationAndroid} <Animated.View style={imageCropStyle}>
accessibilityLabel={imageSrc.alt} <AnimatedImage
accessibilityHint="" contentFit="cover"
accessibilityIgnoresInvertColors source={{uri: imageSrc.uri}}
cachePolicy="memory" placeholderContentFit="cover"
/> placeholder={{uri: imageSrc.thumbUri}}
accessibilityLabel={imageSrc.alt}
onLoad={() => setHasLoaded(false)}
style={imageStyle}
accessibilityHint=""
accessibilityIgnoresInvertColors
cachePolicy="memory"
/>
</Animated.View>
</Animated.View>
</Animated.View> </Animated.View>
</GestureDetector> </GestureDetector>
) )
@@ -352,6 +414,7 @@ const styles = StyleSheet.create({
right: 0, right: 0,
top: 0, top: 0,
bottom: 0, bottom: 0,
justifyContent: 'center',
}, },
}) })
@@ -7,18 +7,28 @@
*/ */
import React, {useState} from 'react' import React, {useState} from 'react'
import {ActivityIndicator, StyleProp, StyleSheet} from 'react-native' import {ActivityIndicator, StyleSheet} from 'react-native'
import { import {
Gesture, Gesture,
GestureDetector, GestureDetector,
PanGesture, PanGesture,
} from 'react-native-gesture-handler' } from 'react-native-gesture-handler'
import Animated, {runOnJS, useAnimatedRef} from 'react-native-reanimated' import Animated, {
runOnJS,
SharedValue,
useAnimatedReaction,
useAnimatedRef,
useAnimatedStyle,
} from 'react-native-reanimated'
import {useSafeAreaFrame} from 'react-native-safe-area-context' import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {Image, ImageStyle} from 'expo-image' import {Image} from 'expo-image'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {Dimensions as ImageDimensions, ImageSource} from '../../@types' import {
Dimensions as ImageDimensions,
ImageSource,
Transform,
} from '../../@types'
const AnimatedImage = Animated.createAnimatedComponent(Image) const AnimatedImage = Animated.createAnimatedComponent(Image)
@@ -40,8 +50,15 @@ type Props = {
} }
imageAspect: number | undefined imageAspect: number | undefined
imageDimensions: ImageDimensions | undefined imageDimensions: ImageDimensions | undefined
imageStyle: StyleProp<ImageStyle>
dismissSwipePan: PanGesture dismissSwipePan: PanGesture
transforms: Readonly<
SharedValue<{
scaleAndMoveTransform: Transform
cropFrameTransform: Transform
cropContentTransform: Transform
isResting: boolean
}>
>
} }
const ImageItem = ({ const ImageItem = ({
@@ -52,8 +69,8 @@ const ImageItem = ({
measureSafeArea, measureSafeArea,
imageAspect, imageAspect,
imageDimensions, imageDimensions,
imageStyle,
dismissSwipePan, dismissSwipePan,
transforms,
}: Props) => { }: Props) => {
const scrollViewRef = useAnimatedRef<Animated.ScrollView>() const scrollViewRef = useAnimatedRef<Animated.ScrollView>()
const [scaled, setScaled] = useState(false) const [scaled, setScaled] = useState(false)
@@ -129,6 +146,55 @@ const ImageItem = ({
singleTap, singleTap,
) )
const containerStyle = useAnimatedStyle(() => {
const {scaleAndMoveTransform} = transforms.value
return {
flex: 1,
transform: scaleAndMoveTransform,
}
})
const imageCropStyle = useAnimatedStyle(() => {
const screenSize = measureSafeArea()
const {cropFrameTransform} = transforms.value
return {
overflow: 'hidden',
transform: cropFrameTransform,
width: screenSize.width,
maxHeight: screenSize.height,
aspectRatio: imageAspect,
alignSelf: 'center',
}
})
const type = imageSrc.type
const borderRadius =
type === 'circle-avi' ? 1e5 : type === 'rect-avi' ? 20 : 0
const imageStyle = useAnimatedStyle(() => {
const {cropContentTransform} = transforms.value
return {
borderRadius,
transform: cropContentTransform,
width: '100%',
aspectRatio: imageAspect,
}
})
const [showLoader, setShowLoader] = useState(false)
const [hasLoaded, setHasLoaded] = useState(false)
useAnimatedReaction(
() => {
return transforms.value.isResting && !hasLoaded
},
(show, prevShow) => {
if (show && !prevShow) {
runOnJS(setShowLoader)(false)
} else if (!prevShow && show) {
runOnJS(setShowLoader)(true)
}
},
)
return ( return (
<GestureDetector gesture={composedGesture}> <GestureDetector gesture={composedGesture}>
<Animated.ScrollView <Animated.ScrollView
@@ -139,21 +205,27 @@ const ImageItem = ({
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
maximumZoomScale={maxZoomScale} maximumZoomScale={maxZoomScale}
onScroll={scrollHandler} onScroll={scrollHandler}
style={containerStyle}
bounces={scaled} bounces={scaled}
bouncesZoom={true} bouncesZoom={true}
centerContent> centerContent>
<ActivityIndicator size="small" color="#FFF" style={styles.loading} /> {showLoader && (
<AnimatedImage <ActivityIndicator size="small" color="#FFF" style={styles.loading} />
contentFit="cover" )}
source={{uri: imageSrc.uri}} <Animated.View style={imageCropStyle}>
placeholderContentFit="cover" <AnimatedImage
placeholder={{uri: imageSrc.thumbUri}} contentFit="contain"
style={imageStyle} source={{uri: imageSrc.uri}}
accessibilityLabel={imageSrc.alt} placeholderContentFit="contain"
accessibilityHint="" placeholder={{uri: imageSrc.thumbUri}}
enableLiveTextInteraction={showControls && !scaled} style={imageStyle}
accessibilityIgnoresInvertColors accessibilityLabel={imageSrc.alt}
/> accessibilityHint=""
enableLiveTextInteraction={showControls && !scaled}
accessibilityIgnoresInvertColors
onLoad={() => setHasLoaded(true)}
/>
</Animated.View>
</Animated.ScrollView> </Animated.ScrollView>
</GestureDetector> </GestureDetector>
) )
@@ -1,11 +1,15 @@
// default implementation fallback for web // default implementation fallback for web
import React from 'react' import React from 'react'
import {ImageStyle, StyleProp, View} from 'react-native' import {View} from 'react-native'
import {PanGesture} from 'react-native-gesture-handler' import {PanGesture} from 'react-native-gesture-handler'
import {BaseAnimationBuilder} from 'react-native-reanimated' import {SharedValue} from 'react-native-reanimated'
import {Dimensions as ImageDimensions, ImageSource} from '../../@types' import {
Dimensions as ImageDimensions,
ImageSource,
Transform,
} from '../../@types'
type Props = { type Props = {
imageSrc: ImageSource imageSrc: ImageSource
@@ -22,9 +26,15 @@ type Props = {
} }
imageAspect: number | undefined imageAspect: number | undefined
imageDimensions: ImageDimensions | undefined imageDimensions: ImageDimensions | undefined
imageStyle: StyleProp<ImageStyle>
dismissSwipePan: PanGesture dismissSwipePan: PanGesture
layoutAnimationAndroid: BaseAnimationBuilder transforms: Readonly<
SharedValue<{
scaleAndMoveTransform: Transform
cropFrameTransform: Transform
cropContentTransform: Transform
isResting: boolean
}>
>
} }
const ImageItem = (_props: Props) => { const ImageItem = (_props: Props) => {
+53 -106
View File
@@ -22,7 +22,6 @@ import Animated, {
AnimatedRef, AnimatedRef,
cancelAnimation, cancelAnimation,
interpolate, interpolate,
LinearTransition,
measure, measure,
runOnJS, runOnJS,
SharedValue, SharedValue,
@@ -45,13 +44,13 @@ import {Trans} from '@lingui/macro'
import {useImageDimensions} from '#/lib/media/image-sizes' import {useImageDimensions} from '#/lib/media/image-sizes'
import {colors, s} from '#/lib/styles' import {colors, s} from '#/lib/styles'
import {isAndroid, isIOS} from '#/platform/detection' import {isIOS} from '#/platform/detection'
import {Lightbox} from '#/state/lightbox' import {Lightbox} from '#/state/lightbox'
import {Button} from '#/view/com/util/forms/Button' import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text' import {Text} from '#/view/com/util/text/Text'
import {ScrollView} from '#/view/com/util/Views' import {ScrollView} from '#/view/com/util/Views'
import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army' import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army'
import {ImageSource} from './@types' import {ImageSource, Transform} from './@types'
import ImageDefaultHeader from './components/ImageDefaultHeader' import ImageDefaultHeader from './components/ImageDefaultHeader'
import ImageItem from './components/ImageItem/ImageItem' import ImageItem from './components/ImageItem/ImageItem'
@@ -65,9 +64,6 @@ const EDGES =
const SLOW_SPRING = {stiffness: 120} const SLOW_SPRING = {stiffness: 120}
const FAST_SPRING = {stiffness: 700} const FAST_SPRING = {stiffness: 700}
const SLOW_SPRING_AS_TRANSITION = LinearTransition.springify()
.overshootClamping(true as any /* Typings are wrong */)
.stiffness(SLOW_SPRING.stiffness)
export default function ImageViewRoot({ export default function ImageViewRoot({
lightbox: nextLightbox, lightbox: nextLightbox,
@@ -84,24 +80,11 @@ export default function ImageViewRoot({
const ref = useAnimatedRef<View>() const ref = useAnimatedRef<View>()
const [activeLightbox, setActiveLightbox] = useState(nextLightbox) const [activeLightbox, setActiveLightbox] = useState(nextLightbox)
const openProgress = useSharedValue(0) const openProgress = useSharedValue(0)
const openProgressTo = useSharedValue(0)
if (!activeLightbox && nextLightbox) { if (!activeLightbox && nextLightbox) {
setActiveLightbox(nextLightbox) setActiveLightbox(nextLightbox)
} }
const updateOpenProgress = React.useCallback(
(toValue: number, animate: boolean) => {
'worklet'
// These *must* always be updated together.
openProgressTo.value = toValue
openProgress.value = animate
? withClampedSpring(toValue, SLOW_SPRING)
: toValue
},
[openProgress, openProgressTo],
)
React.useEffect(() => { React.useEffect(() => {
if (!nextLightbox) { if (!nextLightbox) {
return return
@@ -111,11 +94,17 @@ export default function ImageViewRoot({
!PlatformInfo.getIsReducedMotionEnabled() && !PlatformInfo.getIsReducedMotionEnabled() &&
nextLightbox.images.every(img => img.dimensions && img.thumbRect) nextLightbox.images.every(img => img.dimensions && img.thumbRect)
updateOpenProgress(1, canAnimate) // https://github.com/software-mansion/react-native-reanimated/issues/6677
requestAnimationFrame(() => {
openProgress.value = canAnimate ? withClampedSpring(1, SLOW_SPRING) : 1
})
return () => { return () => {
updateOpenProgress(0, canAnimate) // https://github.com/software-mansion/react-native-reanimated/issues/6677
requestAnimationFrame(() => {
openProgress.value = canAnimate ? withClampedSpring(0, SLOW_SPRING) : 0
})
} }
}, [nextLightbox, updateOpenProgress]) }, [nextLightbox, openProgress])
useAnimatedReaction( useAnimatedReaction(
() => openProgress.value === 0, () => openProgress.value === 0,
@@ -128,9 +117,9 @@ export default function ImageViewRoot({
const onFlyAway = React.useCallback(() => { const onFlyAway = React.useCallback(() => {
'worklet' 'worklet'
updateOpenProgress(0, false) openProgress.value = 0
runOnJS(onRequestClose)() runOnJS(onRequestClose)()
}, [onRequestClose, updateOpenProgress]) }, [onRequestClose, openProgress])
return ( return (
// Keep it always mounted to avoid flicker on the first frame. // Keep it always mounted to avoid flicker on the first frame.
@@ -151,7 +140,6 @@ export default function ImageViewRoot({
onFlyAway={onFlyAway} onFlyAway={onFlyAway}
safeAreaRef={ref} safeAreaRef={ref}
openProgress={openProgress} openProgress={openProgress}
openProgressTo={openProgressTo}
/> />
)} )}
</Animated.View> </Animated.View>
@@ -167,7 +155,6 @@ function ImageView({
onFlyAway, onFlyAway,
safeAreaRef, safeAreaRef,
openProgress, openProgress,
openProgressTo,
}: { }: {
lightbox: Lightbox lightbox: Lightbox
onRequestClose: () => void onRequestClose: () => void
@@ -176,7 +163,6 @@ function ImageView({
onFlyAway: () => void onFlyAway: () => void
safeAreaRef: AnimatedRef<View> safeAreaRef: AnimatedRef<View>
openProgress: SharedValue<number> openProgress: SharedValue<number>
openProgressTo: SharedValue<number>
}) { }) {
const {images, index: initialImageIndex} = lightbox const {images, index: initialImageIndex} = lightbox
const [isScaled, setIsScaled] = useState(false) const [isScaled, setIsScaled] = useState(false)
@@ -303,7 +289,6 @@ function ImageView({
isActive={i === imageIndex} isActive={i === imageIndex}
dismissSwipeTranslateY={dismissSwipeTranslateY} dismissSwipeTranslateY={dismissSwipeTranslateY}
openProgress={openProgress} openProgress={openProgress}
openProgressTo={openProgressTo}
/> />
</View> </View>
))} ))}
@@ -344,7 +329,6 @@ function LightboxImage({
showControls, showControls,
safeAreaRef, safeAreaRef,
openProgress, openProgress,
openProgressTo,
dismissSwipeTranslateY, dismissSwipeTranslateY,
}: { }: {
imageSrc: ImageSource imageSrc: ImageSource
@@ -359,7 +343,6 @@ function LightboxImage({
showControls: boolean showControls: boolean
safeAreaRef: AnimatedRef<View> safeAreaRef: AnimatedRef<View>
openProgress: SharedValue<number> openProgress: SharedValue<number>
openProgressTo: SharedValue<number>
dismissSwipeTranslateY: SharedValue<number> dismissSwipeTranslateY: SharedValue<number>
}) { }) {
const [imageAspect, imageDimensions] = useImageDimensions({ const [imageAspect, imageDimensions] = useImageDimensions({
@@ -392,48 +375,26 @@ function LightboxImage({
safeAreaRef, safeAreaRef,
]) ])
const {thumbRect, dimensions} = imageSrc const {thumbRect} = imageSrc
const interpolation = useDerivedValue(() => { const transforms = useDerivedValue(() => {
'worklet' 'worklet'
const safeArea = measureSafeArea() const safeArea = measureSafeArea()
const finalWidth = safeArea.width
const finalHeight = imageAspect ? safeArea.width / imageAspect : undefined
const dismissTranslateY = const dismissTranslateY =
isActive && openProgress.value === 1 ? dismissSwipeTranslateY.value : 0 isActive && openProgress.value === 1 ? dismissSwipeTranslateY.value : 0
if (isActive && thumbRect && dimensions) {
if (isActive && thumbRect && imageAspect && openProgress.value < 1) {
return interpolateTransform( return interpolateTransform(
openProgress.value, openProgress.value,
openProgressTo.value,
thumbRect, thumbRect,
safeArea, safeArea,
dimensions, imageAspect,
dismissTranslateY,
) )
} }
return { return {
transform: [{translateY: dismissTranslateY}], isResting: dismissTranslateY === 0,
width: finalWidth, scaleAndMoveTransform: [{translateY: dismissTranslateY}],
height: finalHeight, cropFrameTransform: [],
} cropContentTransform: [],
})
const containerStyle = useAnimatedStyle(() => {
const {transform} = interpolation.value
return {
flex: 1,
transform,
}
})
const type = imageSrc.type
const borderRadius =
type === 'circle-avi' ? 1e5 : type === 'rect-avi' ? 20 : 0
const imageStyle = useAnimatedStyle(() => {
const {width, height} = interpolation.value
return {
borderRadius,
width,
height,
} }
}) })
@@ -475,22 +436,19 @@ function LightboxImage({
}) })
return ( return (
<Animated.View style={containerStyle}> <ImageItem
<ImageItem imageSrc={imageSrc}
imageSrc={imageSrc} onTap={onTap}
onTap={onTap} onZoom={onZoom}
onZoom={onZoom} onRequestClose={onRequestClose}
onRequestClose={onRequestClose} isScrollViewBeingDragged={isScrollViewBeingDragged}
isScrollViewBeingDragged={isScrollViewBeingDragged} showControls={showControls}
showControls={showControls} measureSafeArea={measureSafeArea}
measureSafeArea={measureSafeArea} imageAspect={imageAspect}
imageAspect={imageAspect} imageDimensions={imageDimensions}
imageDimensions={imageDimensions} dismissSwipePan={dismissSwipePan}
imageStyle={imageStyle} transforms={transforms}
dismissSwipePan={dismissSwipePan} />
layoutAnimationAndroid={SLOW_SPRING_AS_TRANSITION}
/>
</Animated.View>
) )
} }
@@ -657,7 +615,6 @@ function interpolatePx(
function interpolateTransform( function interpolateTransform(
progress: number, progress: number,
progressTo: number,
thumbnailDims: { thumbnailDims: {
pageX: number pageX: number
width: number width: number
@@ -665,11 +622,14 @@ function interpolateTransform(
height: number height: number
}, },
safeArea: {width: number; height: number; x: number; y: number}, safeArea: {width: number; height: number; x: number; y: number},
imageDims: {width: number; height: number}, imageAspect: number,
dismissTranslateY: number, ): {
) { scaleAndMoveTransform: Transform
cropFrameTransform: Transform
cropContentTransform: Transform
isResting: boolean
} {
'worklet' 'worklet'
const imageAspect = imageDims.width / imageDims.height
const thumbAspect = thumbnailDims.width / thumbnailDims.height const thumbAspect = thumbnailDims.width / thumbnailDims.height
let uncroppedInitialWidth let uncroppedInitialWidth
let uncroppedInitialHeight let uncroppedInitialHeight
@@ -699,34 +659,21 @@ function interpolateTransform(
const scale = interpolate(progress, [0, 1], [initialScale, 1]) const scale = interpolate(progress, [0, 1], [initialScale, 1])
const translateX = interpolatePx(progress, [0, 1], [initialTranslateX, 0]) const translateX = interpolatePx(progress, [0, 1], [initialTranslateX, 0])
const translateY = interpolatePx(progress, [0, 1], [initialTranslateY, 0]) const translateY = interpolatePx(progress, [0, 1], [initialTranslateY, 0])
const cropTranslateX = interpolatePx( const cropScaleX = interpolate(
progress, progress,
[0, 1], [0, 1],
[(finalWidth - croppedFinalWidth) / 2, 0], [croppedFinalWidth / finalWidth, 1],
)
const cropScaleY = interpolate(
progress,
[0, 1],
[croppedFinalHeight / finalHeight, 1],
) )
let width
let height
if (isAndroid) {
// On Android, interpolating `progress` here is too slow and choppy.
// Instead, we'll use discrete `progressTo` and rely on `layout` animation.
width = progressTo === 0 ? croppedFinalWidth : finalWidth
height = progressTo === 0 ? croppedFinalHeight : finalHeight
} else {
// On iOS, interpolating these directly works fine.
// In fact, using the above approach would lead to incorrect positions.
width = interpolatePx(progress, [0, 1], [croppedFinalWidth, finalWidth])
height = interpolatePx(progress, [0, 1], [croppedFinalHeight, finalHeight])
}
return { return {
transform: [ isResting: progress === 1,
{translateY: dismissTranslateY}, scaleAndMoveTransform: [{translateX}, {translateY}, {scale}],
{translateX}, cropFrameTransform: [{scaleX: cropScaleX}, {scaleY: cropScaleY}],
{translateY}, cropContentTransform: [{scaleX: 1 / cropScaleX}, {scaleY: 1 / cropScaleY}],
{scale},
{translateX: cropTranslateX},
],
width,
height,
} }
} }