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