Interpolate width/height on native side for Android
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
PanGesture,
|
PanGesture,
|
||||||
} from 'react-native-gesture-handler'
|
} from 'react-native-gesture-handler'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
|
BaseAnimationBuilder,
|
||||||
runOnJS,
|
runOnJS,
|
||||||
useAnimatedReaction,
|
useAnimatedReaction,
|
||||||
useAnimatedRef,
|
useAnimatedRef,
|
||||||
@@ -50,6 +51,7 @@ type Props = {
|
|||||||
imageDimensions: ImageDimensions | undefined
|
imageDimensions: ImageDimensions | undefined
|
||||||
imageStyle: StyleProp<ImageStyle>
|
imageStyle: StyleProp<ImageStyle>
|
||||||
dismissSwipePan: PanGesture
|
dismissSwipePan: PanGesture
|
||||||
|
layoutAnimationAndroid: BaseAnimationBuilder
|
||||||
}
|
}
|
||||||
const ImageItem = ({
|
const ImageItem = ({
|
||||||
imageSrc,
|
imageSrc,
|
||||||
@@ -61,6 +63,7 @@ const ImageItem = ({
|
|||||||
imageDimensions,
|
imageDimensions,
|
||||||
imageStyle,
|
imageStyle,
|
||||||
dismissSwipePan,
|
dismissSwipePan,
|
||||||
|
layoutAnimationAndroid,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isScaled, setIsScaled] = useState(false)
|
const [isScaled, setIsScaled] = useState(false)
|
||||||
const committedTransform = useSharedValue(initialTransform)
|
const committedTransform = useSharedValue(initialTransform)
|
||||||
@@ -326,6 +329,7 @@ const ImageItem = ({
|
|||||||
placeholderContentFit="cover"
|
placeholderContentFit="cover"
|
||||||
placeholder={{uri: imageSrc.thumbUri}}
|
placeholder={{uri: imageSrc.thumbUri}}
|
||||||
style={imageStyle}
|
style={imageStyle}
|
||||||
|
layout={layoutAnimationAndroid}
|
||||||
accessibilityLabel={imageSrc.alt}
|
accessibilityLabel={imageSrc.alt}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ImageStyle, StyleProp, View} from 'react-native'
|
import {ImageStyle, StyleProp, 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 {Dimensions as ImageDimensions, ImageSource} from '../../@types'
|
import {Dimensions as ImageDimensions, ImageSource} from '../../@types'
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ type Props = {
|
|||||||
imageDimensions: ImageDimensions | undefined
|
imageDimensions: ImageDimensions | undefined
|
||||||
imageStyle: StyleProp<ImageStyle>
|
imageStyle: StyleProp<ImageStyle>
|
||||||
dismissSwipePan: PanGesture
|
dismissSwipePan: PanGesture
|
||||||
|
layoutAnimationAndroid: BaseAnimationBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageItem = (_props: Props) => {
|
const ImageItem = (_props: Props) => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import Animated, {
|
|||||||
AnimatedRef,
|
AnimatedRef,
|
||||||
cancelAnimation,
|
cancelAnimation,
|
||||||
interpolate,
|
interpolate,
|
||||||
|
LinearTransition,
|
||||||
measure,
|
measure,
|
||||||
runOnJS,
|
runOnJS,
|
||||||
SharedValue,
|
SharedValue,
|
||||||
@@ -44,7 +45,7 @@ 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 {isIOS} from '#/platform/detection'
|
import {isAndroid, 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'
|
||||||
@@ -57,13 +58,17 @@ import ImageItem from './components/ImageItem/ImageItem'
|
|||||||
type Rect = {x: number; y: number; width: number; height: number}
|
type Rect = {x: number; y: number; width: number; height: number}
|
||||||
|
|
||||||
const PIXEL_RATIO = PixelRatio.get()
|
const PIXEL_RATIO = PixelRatio.get()
|
||||||
const SLOW_SPRING = {stiffness: 120}
|
|
||||||
const FAST_SPRING = {stiffness: 700}
|
|
||||||
const EDGES =
|
const EDGES =
|
||||||
Platform.OS === 'android'
|
Platform.OS === 'android'
|
||||||
? (['top', 'bottom', 'left', 'right'] satisfies Edge[])
|
? (['top', 'bottom', 'left', 'right'] satisfies Edge[])
|
||||||
: (['left', 'right'] satisfies Edge[]) // iOS, so no top/bottom safe area
|
: (['left', 'right'] satisfies Edge[]) // iOS, so no top/bottom safe area
|
||||||
|
|
||||||
|
const SLOW_SPRING = {stiffness: 120}
|
||||||
|
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,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
@@ -75,33 +80,42 @@ export default function ImageViewRoot({
|
|||||||
onPressSave: (uri: string) => void
|
onPressSave: (uri: string) => void
|
||||||
onPressShare: (uri: string) => void
|
onPressShare: (uri: string) => void
|
||||||
}) {
|
}) {
|
||||||
|
'use no memo'
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
const canAnimate =
|
const canAnimate =
|
||||||
!PlatformInfo.getIsReducedMotionEnabled() &&
|
!PlatformInfo.getIsReducedMotionEnabled() &&
|
||||||
nextLightbox.images.every(img => img.dimensions && img.thumbRect)
|
nextLightbox.images.every(img => img.dimensions && img.thumbRect)
|
||||||
if (canAnimate) {
|
|
||||||
openProgress.value = withClampedSpring(1, SLOW_SPRING)
|
updateOpenProgress(1, canAnimate)
|
||||||
return () => {
|
return () => {
|
||||||
openProgress.value = withClampedSpring(0, SLOW_SPRING)
|
updateOpenProgress(0, canAnimate)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
openProgress.value = 1
|
|
||||||
return () => {
|
|
||||||
openProgress.value = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [nextLightbox, openProgress])
|
}, [nextLightbox, updateOpenProgress])
|
||||||
|
|
||||||
useAnimatedReaction(
|
useAnimatedReaction(
|
||||||
() => openProgress.value === 0,
|
() => openProgress.value === 0,
|
||||||
@@ -114,9 +128,9 @@ export default function ImageViewRoot({
|
|||||||
|
|
||||||
const onFlyAway = React.useCallback(() => {
|
const onFlyAway = React.useCallback(() => {
|
||||||
'worklet'
|
'worklet'
|
||||||
openProgress.value = 0
|
updateOpenProgress(0, false)
|
||||||
runOnJS(onRequestClose)()
|
runOnJS(onRequestClose)()
|
||||||
}, [onRequestClose, openProgress])
|
}, [onRequestClose, updateOpenProgress])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Keep it always mounted to avoid flicker on the first frame.
|
// Keep it always mounted to avoid flicker on the first frame.
|
||||||
@@ -137,6 +151,7 @@ export default function ImageViewRoot({
|
|||||||
onFlyAway={onFlyAway}
|
onFlyAway={onFlyAway}
|
||||||
safeAreaRef={ref}
|
safeAreaRef={ref}
|
||||||
openProgress={openProgress}
|
openProgress={openProgress}
|
||||||
|
openProgressTo={openProgressTo}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -152,6 +167,7 @@ function ImageView({
|
|||||||
onFlyAway,
|
onFlyAway,
|
||||||
safeAreaRef,
|
safeAreaRef,
|
||||||
openProgress,
|
openProgress,
|
||||||
|
openProgressTo,
|
||||||
}: {
|
}: {
|
||||||
lightbox: Lightbox
|
lightbox: Lightbox
|
||||||
onRequestClose: () => void
|
onRequestClose: () => void
|
||||||
@@ -160,6 +176,7 @@ 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)
|
||||||
@@ -286,6 +303,7 @@ function ImageView({
|
|||||||
isActive={i === imageIndex}
|
isActive={i === imageIndex}
|
||||||
dismissSwipeTranslateY={dismissSwipeTranslateY}
|
dismissSwipeTranslateY={dismissSwipeTranslateY}
|
||||||
openProgress={openProgress}
|
openProgress={openProgress}
|
||||||
|
openProgressTo={openProgressTo}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
@@ -326,6 +344,7 @@ function LightboxImage({
|
|||||||
showControls,
|
showControls,
|
||||||
safeAreaRef,
|
safeAreaRef,
|
||||||
openProgress,
|
openProgress,
|
||||||
|
openProgressTo,
|
||||||
dismissSwipeTranslateY,
|
dismissSwipeTranslateY,
|
||||||
}: {
|
}: {
|
||||||
imageSrc: ImageSource
|
imageSrc: ImageSource
|
||||||
@@ -340,6 +359,7 @@ 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({
|
||||||
@@ -378,17 +398,20 @@ function LightboxImage({
|
|||||||
const safeArea = measureSafeArea()
|
const safeArea = measureSafeArea()
|
||||||
const finalWidth = safeArea.width
|
const finalWidth = safeArea.width
|
||||||
const finalHeight = imageAspect ? safeArea.width / imageAspect : undefined
|
const finalHeight = imageAspect ? safeArea.width / imageAspect : undefined
|
||||||
if (isActive && thumbRect && dimensions && openProgress.value < 1) {
|
const dismissTranslateY =
|
||||||
|
isActive && openProgress.value === 1 ? dismissSwipeTranslateY.value : 0
|
||||||
|
if (isActive && thumbRect && dimensions) {
|
||||||
return interpolateTransform(
|
return interpolateTransform(
|
||||||
openProgress.value,
|
openProgress.value,
|
||||||
|
openProgressTo.value,
|
||||||
thumbRect,
|
thumbRect,
|
||||||
safeArea,
|
safeArea,
|
||||||
dimensions,
|
dimensions,
|
||||||
|
dismissTranslateY,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const translateY = isActive ? dismissSwipeTranslateY.value : 0
|
|
||||||
return {
|
return {
|
||||||
transform: [{translateY}],
|
transform: [{translateY: dismissTranslateY}],
|
||||||
width: finalWidth,
|
width: finalWidth,
|
||||||
height: finalHeight,
|
height: finalHeight,
|
||||||
}
|
}
|
||||||
@@ -465,6 +488,7 @@ function LightboxImage({
|
|||||||
imageDimensions={imageDimensions}
|
imageDimensions={imageDimensions}
|
||||||
imageStyle={imageStyle}
|
imageStyle={imageStyle}
|
||||||
dismissSwipePan={dismissSwipePan}
|
dismissSwipePan={dismissSwipePan}
|
||||||
|
layoutAnimationAndroid={SLOW_SPRING_AS_TRANSITION}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
@@ -633,6 +657,7 @@ function interpolatePx(
|
|||||||
|
|
||||||
function interpolateTransform(
|
function interpolateTransform(
|
||||||
progress: number,
|
progress: number,
|
||||||
|
progressTo: number,
|
||||||
thumbnailDims: {
|
thumbnailDims: {
|
||||||
pageX: number
|
pageX: number
|
||||||
width: number
|
width: number
|
||||||
@@ -641,6 +666,7 @@ function interpolateTransform(
|
|||||||
},
|
},
|
||||||
safeArea: {width: number; height: number; x: number; y: number},
|
safeArea: {width: number; height: number; x: number; y: number},
|
||||||
imageDims: {width: number; height: number},
|
imageDims: {width: number; height: number},
|
||||||
|
dismissTranslateY: number,
|
||||||
) {
|
) {
|
||||||
'worklet'
|
'worklet'
|
||||||
const imageAspect = imageDims.width / imageDims.height
|
const imageAspect = imageDims.width / imageDims.height
|
||||||
@@ -673,19 +699,27 @@ 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 width = interpolatePx(progress, [0, 1], [croppedFinalWidth, finalWidth])
|
|
||||||
const height = interpolatePx(
|
|
||||||
progress,
|
|
||||||
[0, 1],
|
|
||||||
[croppedFinalHeight, finalHeight],
|
|
||||||
)
|
|
||||||
const cropTranslateX = interpolatePx(
|
const cropTranslateX = interpolatePx(
|
||||||
progress,
|
progress,
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[(finalWidth - croppedFinalWidth) / 2, 0],
|
[(finalWidth - croppedFinalWidth) / 2, 0],
|
||||||
)
|
)
|
||||||
|
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: [
|
transform: [
|
||||||
|
{translateY: dismissTranslateY},
|
||||||
{translateX},
|
{translateX},
|
||||||
{translateY},
|
{translateY},
|
||||||
{scale},
|
{scale},
|
||||||
|
|||||||
Reference in New Issue
Block a user