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
+2 -1
View File
@@ -4,7 +4,7 @@ import {Image} from 'expo-image'
import {useLightboxControls} from '#/state/lightbox' import {useLightboxControls} from '#/state/lightbox'
import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types' import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
import {atoms as a} from '#/alf' import {atoms as a, tokens} from '#/alf'
import {AutoSizedImage} from '#/components/images/AutoSizedImage' import {AutoSizedImage} from '#/components/images/AutoSizedImage'
import {Gallery} from '#/components/images/Gallery' import {Gallery} from '#/components/images/Gallery'
import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid' import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid'
@@ -42,6 +42,7 @@ export function ImageEmbed({
thumbRect: null, thumbRect: null,
thumbRef: refs[i] ?? null, thumbRef: refs[i] ?? null,
thumbDimensions: fetchedDims[i] ?? null, thumbDimensions: fetchedDims[i] ?? null,
thumbBorderRadius: tokens.borderRadius.md,
type: 'image', type: 'image',
})), })),
index, index,
@@ -29,6 +29,7 @@ export type ImageSource = {
thumbDimensions: Dimensions | null thumbDimensions: Dimensions | null
thumbRect: MeasuredDimensions | null thumbRect: MeasuredDimensions | null
thumbRef?: AnimatedRef<any> | null thumbRef?: AnimatedRef<any> | null
thumbBorderRadius?: number
alt?: string alt?: string
type: 'image' | 'circle-avi' | 'rect-avi' type: 'image' | 'circle-avi' | 'rect-avi'
} }
@@ -37,3 +38,12 @@ export type Transform = Exclude<
TransformsStyle['transform'], TransformsStyle['transform'],
string | undefined 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 { import {
type Dimensions as ImageDimensions, type Dimensions as ImageDimensions,
type ImageSource, type ImageSource,
type Transform, type LightboxTransforms,
} from '../../@types' } from '../../@types'
import { import {
applyRounding, applyRounding,
@@ -53,15 +53,7 @@ type Props = {
imageAspect: number | undefined imageAspect: number | undefined
imageDimensions: ImageDimensions | undefined imageDimensions: ImageDimensions | undefined
dismissSwipePan: PanGesture dismissSwipePan: PanGesture
transforms: Readonly< transforms: Readonly<SharedValue<LightboxTransforms>>
SharedValue<{
scaleAndMoveTransform: Transform
cropFrameTransform: Transform
cropContentTransform: Transform
isResting: boolean
isHidden: boolean
}>
>
} }
const ImageItem = ({ const ImageItem = ({
imageSrc, imageSrc,
@@ -339,11 +331,12 @@ const ImageItem = ({
}) })
const imageCropStyle = useAnimatedStyle(() => { const imageCropStyle = useAnimatedStyle(() => {
const {cropFrameTransform} = transforms.get() const {cropFrameTransform, borderRadius: br} = transforms.get()
return { return {
flex: 1, flex: 1,
overflow: 'hidden', overflow: 'hidden',
transform: cropFrameTransform, transform: cropFrameTransform,
borderRadius: br,
} }
}) })
@@ -29,7 +29,7 @@ import {Image} from 'expo-image'
import { import {
type Dimensions as ImageDimensions, type Dimensions as ImageDimensions,
type ImageSource, type ImageSource,
type Transform, type LightboxTransforms,
} from '../../@types' } from '../../@types'
const MAX_ORIGINAL_IMAGE_ZOOM = 2 const MAX_ORIGINAL_IMAGE_ZOOM = 2
@@ -52,15 +52,7 @@ type Props = {
imageAspect: number | undefined imageAspect: number | undefined
imageDimensions: ImageDimensions | undefined imageDimensions: ImageDimensions | undefined
dismissSwipePan: PanGesture dismissSwipePan: PanGesture
transforms: Readonly< transforms: Readonly<SharedValue<LightboxTransforms>>
SharedValue<{
scaleAndMoveTransform: Transform
cropFrameTransform: Transform
cropContentTransform: Transform
isResting: boolean
isHidden: boolean
}>
>
} }
const ImageItem = ({ const ImageItem = ({
@@ -170,10 +162,11 @@ const ImageItem = ({
const imageCropStyle = useAnimatedStyle(() => { const imageCropStyle = useAnimatedStyle(() => {
const screenSize = measureSafeArea() const screenSize = measureSafeArea()
const {cropFrameTransform} = transforms.get() const {cropFrameTransform, borderRadius: br} = transforms.get()
return { return {
overflow: 'hidden', overflow: 'hidden',
transform: cropFrameTransform, transform: cropFrameTransform,
borderRadius: br,
width: screenSize.width, width: screenSize.width,
maxHeight: screenSize.height, maxHeight: screenSize.height,
alignSelf: 'center', alignSelf: 'center',
@@ -9,7 +9,7 @@ import {type Dimensions} from '#/lib/media/types'
import { import {
type Dimensions as ImageDimensions, type Dimensions as ImageDimensions,
type ImageSource, type ImageSource,
type Transform, type LightboxTransforms,
} from '../../@types' } from '../../@types'
type Props = { type Props = {
@@ -29,15 +29,7 @@ type Props = {
imageAspect: number | undefined imageAspect: number | undefined
imageDimensions: ImageDimensions | undefined imageDimensions: ImageDimensions | undefined
dismissSwipePan: PanGesture dismissSwipePan: PanGesture
transforms: Readonly< transforms: Readonly<SharedValue<LightboxTransforms>>
SharedValue<{
scaleAndMoveTransform: Transform
cropFrameTransform: Transform
cropContentTransform: Transform
isResting: boolean
isHidden: boolean
}>
>
} }
const ImageItem = (_props: Props) => { const ImageItem = (_props: Props) => {
+23 -3
View File
@@ -52,7 +52,11 @@ import {useTheme} from '#/alf'
import {setSystemUITheme} from '#/alf/util/systemUI' import {setSystemUITheme} from '#/alf/util/systemUI'
import {IS_IOS} from '#/env' import {IS_IOS} from '#/env'
import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army' 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 ImageDefaultHeader from './components/ImageDefaultHeader'
import ImageItem from './components/ImageItem/ImageItem' import ImageItem from './components/ImageItem/ImageItem'
@@ -494,8 +498,8 @@ function LightboxImage({
return safeArea return safeArea
}, [safeAreaRef, heightDelayedForJSThreadOnly, widthDelayedForJSThreadOnly]) }, [safeAreaRef, heightDelayedForJSThreadOnly, widthDelayedForJSThreadOnly])
const {thumbRect: thumbRectJS} = imageSrc const {thumbRect: thumbRectJS, thumbBorderRadius} = imageSrc
const transforms = useDerivedValue(() => { const transforms = useDerivedValue<LightboxTransforms>(() => {
'worklet' 'worklet'
const safeArea = measureSafeArea() const safeArea = measureSafeArea()
const openProgressValue = openProgress.get() const openProgressValue = openProgress.get()
@@ -506,6 +510,7 @@ function LightboxImage({
return { return {
isHidden: true, isHidden: true,
isResting: false, isResting: false,
borderRadius: 0,
scaleAndMoveTransform: [], scaleAndMoveTransform: [],
cropFrameTransform: [], cropFrameTransform: [],
cropContentTransform: [], cropContentTransform: [],
@@ -525,12 +530,14 @@ function LightboxImage({
thumbRect, thumbRect,
safeArea, safeArea,
imageAspect, imageAspect,
thumbBorderRadius,
) )
} }
} }
return { return {
isHidden: false, isHidden: false,
isResting: dismissTranslateY === 0, isResting: dismissTranslateY === 0,
borderRadius: 0,
scaleAndMoveTransform: [{translateY: dismissTranslateY}], scaleAndMoveTransform: [{translateY: dismissTranslateY}],
cropFrameTransform: [], cropFrameTransform: [],
cropContentTransform: [], cropContentTransform: [],
@@ -772,10 +779,12 @@ function interpolateTransform(
}, },
safeArea: {width: number; height: number; x: number; y: number}, safeArea: {width: number; height: number; x: number; y: number},
imageAspect: number, imageAspect: number,
thumbBorderRadius?: number,
): { ): {
scaleAndMoveTransform: Transform scaleAndMoveTransform: Transform
cropFrameTransform: Transform cropFrameTransform: Transform
cropContentTransform: Transform cropContentTransform: Transform
borderRadius: number
isResting: boolean isResting: boolean
isHidden: boolean isHidden: boolean
} { } {
@@ -827,12 +836,23 @@ function interpolateTransform(
[0, 1], [0, 1],
[croppedFinalHeight / finalHeight, 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 { return {
isHidden: false, isHidden: false,
isResting: progress === 1, isResting: progress === 1,
scaleAndMoveTransform: [{translateX}, {translateY}, {scale}], scaleAndMoveTransform: [{translateX}, {translateY}, {scale}],
cropFrameTransform: [{scaleX: cropScaleX}, {scaleY: cropScaleY}], cropFrameTransform: [{scaleX: cropScaleX}, {scaleY: cropScaleY}],
cropContentTransform: [{scaleX: 1 / cropScaleX}, {scaleY: 1 / cropScaleY}], cropContentTransform: [{scaleX: 1 / cropScaleX}, {scaleY: 1 / cropScaleY}],
borderRadius,
} }
} }