Animate border radius in lightbox transition (#10272)

This commit is contained in:
Samuel Newman
2026-04-17 05:45:56 -07:00
committed by GitHub
parent a77b6e3525
commit e58feaeb0f
6 changed files with 45 additions and 36 deletions
@@ -29,6 +29,7 @@ export type ImageSource = {
thumbDimensions: Dimensions | null
thumbRect: MeasuredDimensions | null
thumbRef?: AnimatedRef<any> | null
thumbBorderRadius?: number
alt?: string
type: 'image' | 'circle-avi' | 'rect-avi'
}
@@ -37,3 +38,12 @@ export type Transform = Exclude<
TransformsStyle['transform'],
string | undefined
>
export type LightboxTransforms = {
scaleAndMoveTransform: Transform
cropFrameTransform: Transform
cropContentTransform: Transform
borderRadius: number
isResting: boolean
isHidden: boolean
}
@@ -19,7 +19,7 @@ import {Image} from 'expo-image'
import {
type Dimensions as ImageDimensions,
type ImageSource,
type Transform,
type LightboxTransforms,
} from '../../@types'
import {
applyRounding,
@@ -53,15 +53,7 @@ type Props = {
imageAspect: number | undefined
imageDimensions: ImageDimensions | undefined
dismissSwipePan: PanGesture
transforms: Readonly<
SharedValue<{
scaleAndMoveTransform: Transform
cropFrameTransform: Transform
cropContentTransform: Transform
isResting: boolean
isHidden: boolean
}>
>
transforms: Readonly<SharedValue<LightboxTransforms>>
}
const ImageItem = ({
imageSrc,
@@ -339,11 +331,12 @@ const ImageItem = ({
})
const imageCropStyle = useAnimatedStyle(() => {
const {cropFrameTransform} = transforms.get()
const {cropFrameTransform, borderRadius: br} = transforms.get()
return {
flex: 1,
overflow: 'hidden',
transform: cropFrameTransform,
borderRadius: br,
}
})
@@ -29,7 +29,7 @@ import {Image} from 'expo-image'
import {
type Dimensions as ImageDimensions,
type ImageSource,
type Transform,
type LightboxTransforms,
} from '../../@types'
const MAX_ORIGINAL_IMAGE_ZOOM = 2
@@ -52,15 +52,7 @@ type Props = {
imageAspect: number | undefined
imageDimensions: ImageDimensions | undefined
dismissSwipePan: PanGesture
transforms: Readonly<
SharedValue<{
scaleAndMoveTransform: Transform
cropFrameTransform: Transform
cropContentTransform: Transform
isResting: boolean
isHidden: boolean
}>
>
transforms: Readonly<SharedValue<LightboxTransforms>>
}
const ImageItem = ({
@@ -170,10 +162,11 @@ const ImageItem = ({
const imageCropStyle = useAnimatedStyle(() => {
const screenSize = measureSafeArea()
const {cropFrameTransform} = transforms.get()
const {cropFrameTransform, borderRadius: br} = transforms.get()
return {
overflow: 'hidden',
transform: cropFrameTransform,
borderRadius: br,
width: screenSize.width,
maxHeight: screenSize.height,
alignSelf: 'center',
@@ -9,7 +9,7 @@ import {type Dimensions} from '#/lib/media/types'
import {
type Dimensions as ImageDimensions,
type ImageSource,
type Transform,
type LightboxTransforms,
} from '../../@types'
type Props = {
@@ -29,15 +29,7 @@ type Props = {
imageAspect: number | undefined
imageDimensions: ImageDimensions | undefined
dismissSwipePan: PanGesture
transforms: Readonly<
SharedValue<{
scaleAndMoveTransform: Transform
cropFrameTransform: Transform
cropContentTransform: Transform
isResting: boolean
isHidden: boolean
}>
>
transforms: Readonly<SharedValue<LightboxTransforms>>
}
const ImageItem = (_props: Props) => {
+23 -3
View File
@@ -52,7 +52,11 @@ import {useTheme} from '#/alf'
import {setSystemUITheme} from '#/alf/util/systemUI'
import {IS_IOS} from '#/env'
import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army'
import {type ImageSource, type Transform} from './@types'
import {
type ImageSource,
type LightboxTransforms,
type Transform,
} from './@types'
import ImageDefaultHeader from './components/ImageDefaultHeader'
import ImageItem from './components/ImageItem/ImageItem'
@@ -494,8 +498,8 @@ function LightboxImage({
return safeArea
}, [safeAreaRef, heightDelayedForJSThreadOnly, widthDelayedForJSThreadOnly])
const {thumbRect: thumbRectJS} = imageSrc
const transforms = useDerivedValue(() => {
const {thumbRect: thumbRectJS, thumbBorderRadius} = imageSrc
const transforms = useDerivedValue<LightboxTransforms>(() => {
'worklet'
const safeArea = measureSafeArea()
const openProgressValue = openProgress.get()
@@ -506,6 +510,7 @@ function LightboxImage({
return {
isHidden: true,
isResting: false,
borderRadius: 0,
scaleAndMoveTransform: [],
cropFrameTransform: [],
cropContentTransform: [],
@@ -525,12 +530,14 @@ function LightboxImage({
thumbRect,
safeArea,
imageAspect,
thumbBorderRadius,
)
}
}
return {
isHidden: false,
isResting: dismissTranslateY === 0,
borderRadius: 0,
scaleAndMoveTransform: [{translateY: dismissTranslateY}],
cropFrameTransform: [],
cropContentTransform: [],
@@ -772,10 +779,12 @@ function interpolateTransform(
},
safeArea: {width: number; height: number; x: number; y: number},
imageAspect: number,
thumbBorderRadius?: number,
): {
scaleAndMoveTransform: Transform
cropFrameTransform: Transform
cropContentTransform: Transform
borderRadius: number
isResting: boolean
isHidden: boolean
} {
@@ -827,12 +836,23 @@ function interpolateTransform(
[0, 1],
[croppedFinalHeight / finalHeight, 1],
)
// The border radius in the source thumbnail needs to be scaled to account
// for the crop frame and overall scale so it visually matches at progress=0.
const sourceBorderRadius = thumbBorderRadius ?? 0
const initialCropScaleX = croppedFinalWidth / finalWidth
const borderRadius = interpolate(
progress,
[0, 1],
[sourceBorderRadius / (initialScale * initialCropScaleX), 0],
)
return {
isHidden: false,
isResting: progress === 1,
scaleAndMoveTransform: [{translateX}, {translateY}, {scale}],
cropFrameTransform: [{scaleX: cropScaleX}, {scaleY: cropScaleY}],
cropContentTransform: [{scaleX: 1 / cropScaleX}, {scaleY: 1 / cropScaleY}],
borderRadius,
}
}