Support animating out of different index

This commit is contained in:
Dan Abramov
2024-11-02 04:08:23 +00:00
parent b472d54b34
commit 24283427fd
8 changed files with 70 additions and 51 deletions
@@ -6,6 +6,8 @@
*
*/
import {MeasuredDimensions} from 'react-native-reanimated'
export type Dimensions = {
width: number
height: number
@@ -19,6 +21,7 @@ export type Position = {
export type ImageSource = {
uri: string
thumbUri: string
thumbRect: MeasuredDimensions | null
alt?: string
dimensions: Dimensions | null
}
+19 -29
View File
@@ -54,7 +54,6 @@ function ImageViewing({
lightbox,
openProgress,
onFlyAway,
onChangeIndex,
onRequestClose,
onPressSave,
onPressShare,
@@ -62,12 +61,11 @@ function ImageViewing({
lightbox: Lightbox
openProgress: SharedValue<number>
onFlyAway: () => void
onChangeIndex: (index: number) => void
onRequestClose: () => void
onPressSave: (uri: string) => void
onPressShare: (uri: string) => void
}) {
const {images, index: initialImageIndex, thumbDims} = lightbox
const {images, index: initialImageIndex} = lightbox
const [isScaled, setIsScaled] = useState(false)
const [isDragging, setIsDragging] = useState(false)
const [imageIndex, setImageIndex] = useState(initialImageIndex)
@@ -106,12 +104,13 @@ function ImageViewing({
transform: [{translateY: dismissSwipeTranslateY.value}],
}
}
if (thumbDims && images[imageIndex].dimensions) {
const image = images[imageIndex]
if (image.thumbRect && image.dimensions) {
const interpolatedTransform = interpolateTransform(
openProgress.value,
thumbDims,
image.thumbRect,
SCREEN,
images[imageIndex].dimensions,
image.dimensions,
)
return {
pointerEvents: 'none',
@@ -209,7 +208,6 @@ function ImageViewing({
onPageSelected={e => {
setImageIndex(e.nativeEvent.position)
setIsScaled(false)
onChangeIndex(e.nativeEvent.position)
}}
onPageScrollStateChanged={e => {
setIsDragging(e.nativeEvent.pageScrollState !== 'idle')
@@ -427,35 +425,30 @@ function ImageViewingRoot({
}) {
const [activeLightbox, setActiveLightbox] = useState(nextLightbox)
const openProgress = useSharedValue(0)
const isAnimatable = useSharedValue(false)
const isInitialIndex = useSharedValue(true)
if (!activeLightbox && nextLightbox) {
setActiveLightbox(nextLightbox)
}
React.useEffect(() => {
if (nextLightbox) {
if (
nextLightbox.images[nextLightbox.index].dimensions &&
nextLightbox.thumbDims
) {
openProgress.value = withClampedSpring(1)
isAnimatable.value = true
} else {
openProgress.value = 1
isAnimatable.value = false
}
isInitialIndex.value = true
} else {
// TODO: Support animation to non-initial index.
if (isAnimatable.value && isInitialIndex.value) {
if (!nextLightbox) {
return
}
const canAnimate = nextLightbox.images.every(
img => img.dimensions && img.thumbRect,
)
if (canAnimate) {
openProgress.value = withClampedSpring(1)
return () => {
openProgress.value = withClampedSpring(0)
} else {
}
} else {
openProgress.value = 1
return () => {
openProgress.value = 0
}
}
}, [nextLightbox, openProgress, isAnimatable, isInitialIndex])
}, [nextLightbox, openProgress])
useAnimatedReaction(
() => openProgress.value === 0,
@@ -476,9 +469,6 @@ function ImageViewingRoot({
lightbox={activeLightbox}
openProgress={openProgress}
onRequestClose={onRequestClose}
onChangeIndex={index => {
isInitialIndex.value = index === activeLightbox.index
}}
onFlyAway={() => {
'worklet'
openProgress.value = 0
@@ -75,6 +75,7 @@ export function ProfileSubpageHeader({
{
uri: avatar,
thumbUri: avatar,
thumbRect: null,
dimensions: {
// It's fine if it's actually smaller but we know it's 1:1.
height: 1000,
@@ -83,7 +84,6 @@ export function ProfileSubpageHeader({
},
],
index: 0,
thumbDims: null,
})
}
}, [openLightbox, avatar])
+6 -5
View File
@@ -1,6 +1,6 @@
import React from 'react'
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
import Animated, {AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
import Animated, {AnimatedRef} from 'react-native-reanimated'
import {Image, ImageStyle} from 'expo-image'
import {AppBskyEmbedImages} from '@atproto/api'
import {msg} from '@lingui/macro'
@@ -19,13 +19,14 @@ interface Props {
index: number
onPress?: (
index: number,
containerRef: AnimatedRef<React.Component<{}, {}, any>>,
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[],
) => void
onLongPress?: EventFunction
onPressIn?: EventFunction
imageStyle?: StyleProp<ImageStyle>
viewContext?: PostEmbedViewContext
insetBorderStyle?: StyleProp<ViewStyle>
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[]
}
export function GalleryItem({
@@ -37,6 +38,7 @@ export function GalleryItem({
onLongPress,
viewContext,
insetBorderStyle,
containerRefs,
}: Props) {
const t = useTheme()
const {_} = useLingui()
@@ -45,11 +47,10 @@ export function GalleryItem({
const hasAlt = !!image.alt
const hideBadges =
viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
const containerRef = useAnimatedRef()
return (
<Animated.View style={a.flex_1} ref={containerRef}>
<Animated.View style={a.flex_1} ref={containerRefs[index]}>
<Pressable
onPress={onPress ? () => onPress(index, containerRef) : undefined}
onPress={onPress ? () => onPress(index, containerRefs) : undefined}
onPressIn={onPressIn ? () => onPressIn(index) : undefined}
onLongPress={onLongPress ? () => onLongPress(index) : undefined}
style={[
+31 -6
View File
@@ -1,6 +1,6 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {AnimatedRef} from 'react-native-reanimated'
import {AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
import {AppBskyEmbedImages} from '@atproto/api'
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
@@ -11,7 +11,7 @@ interface ImageLayoutGridProps {
images: AppBskyEmbedImages.ViewImage[]
onPress?: (
index: number,
containerRef: AnimatedRef<React.Component<{}, {}, any>>,
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[],
) => void
onLongPress?: (index: number) => void
onPressIn?: (index: number) => void
@@ -42,7 +42,7 @@ interface ImageLayoutGridInnerProps {
images: AppBskyEmbedImages.ViewImage[]
onPress?: (
index: number,
containerRef: AnimatedRef<React.Component<{}, {}, any>>,
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[],
) => void
onLongPress?: (index: number) => void
onPressIn?: (index: number) => void
@@ -54,8 +54,14 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
const gap = props.gap
const count = props.images.length
const containerRef1 = useAnimatedRef()
const containerRef2 = useAnimatedRef()
const containerRef3 = useAnimatedRef()
const containerRef4 = useAnimatedRef()
switch (count) {
case 2:
case 2: {
const containerRefs = [containerRef1, containerRef2]
return (
<View style={[a.flex_1, a.flex_row, gap]}>
<View style={[a.flex_1, {aspectRatio: 1}]}>
@@ -63,6 +69,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
{...props}
index={0}
insetBorderStyle={noCorners(['topRight', 'bottomRight'])}
containerRefs={containerRefs}
/>
</View>
<View style={[a.flex_1, {aspectRatio: 1}]}>
@@ -70,12 +77,15 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
{...props}
index={1}
insetBorderStyle={noCorners(['topLeft', 'bottomLeft'])}
containerRefs={containerRefs}
/>
</View>
</View>
)
}
case 3:
case 3: {
const containerRefs = [containerRef1, containerRef2, containerRef3]
return (
<View style={[a.flex_1, a.flex_row, gap]}>
<View style={[a.flex_1]}>
@@ -83,6 +93,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
{...props}
index={0}
insetBorderStyle={noCorners(['topRight', 'bottomRight'])}
containerRefs={containerRefs}
/>
</View>
<View style={[a.flex_1, gap]}>
@@ -95,6 +106,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
'bottomLeft',
'bottomRight',
])}
containerRefs={containerRefs}
/>
</View>
<View style={[a.flex_1]}>
@@ -106,13 +118,21 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
'bottomLeft',
'topRight',
])}
containerRefs={containerRefs}
/>
</View>
</View>
</View>
)
}
case 4:
case 4: {
const containerRefs = [
containerRef1,
containerRef2,
containerRef3,
containerRef4,
]
return (
<>
<View style={[a.flex_row, gap]}>
@@ -125,6 +145,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
'topRight',
'bottomRight',
])}
containerRefs={containerRefs}
/>
</View>
<View style={[a.flex_1, {aspectRatio: 1.5}]}>
@@ -136,6 +157,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
'bottomLeft',
'bottomRight',
])}
containerRefs={containerRefs}
/>
</View>
</View>
@@ -149,6 +171,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
'topRight',
'bottomRight',
])}
containerRefs={containerRefs}
/>
</View>
<View style={[a.flex_1, {aspectRatio: 1.5}]}>
@@ -160,11 +183,13 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
'bottomLeft',
'topRight',
])}
containerRefs={containerRefs}
/>
</View>
</View>
</>
)
}
default:
return null
+9 -7
View File
@@ -149,22 +149,24 @@ export function PostEmbeds({
}))
const _openLightbox = (
index: number,
thumbDims: MeasuredDimensions | null,
thumbRects: (MeasuredDimensions | null)[],
) => {
openLightbox({
images: items,
images: items.map((item, i) => ({
...item,
thumbRect: thumbRects[i] ?? null,
})),
index,
thumbDims,
})
}
const onPress = (
index: number,
ref: AnimatedRef<React.Component<{}, {}, any>>,
refs: AnimatedRef<React.Component<{}, {}, any>>[],
) => {
runOnUI(() => {
'worklet'
const dims = measure(ref)
runOnJS(_openLightbox)(index, dims)
const rects = refs.map(ref => (ref ? measure(ref) : null))
runOnJS(_openLightbox)(index, rects)
})()
}
const onPressIn = (_: number) => {
@@ -188,7 +190,7 @@ export function PostEmbeds({
: 'constrained'
}
image={image}
onPress={() => onPress(0, containerRef)}
onPress={() => onPress(0, [containerRef])}
onPressIn={() => onPressIn(0)}
hideBadge={
viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia