[Lightbox] Open animation (#6159)

* Measure all rects for embeds

* Measure avi rects too

* Animate lightbox in and out

* Account for safe area in the animation

* Tune spring times

* Remove null checks for measurements

* Remove superfluous view

* Block swipe while opening

* Interpolate width/height on native side for Android

* Make it fast by animating only affine transforms

* Fix tall image final state

The initial animation frame is still off on both platforms.

* Try to squeeze perf

* Avoid blank images during animation on iOS

* Fix bad rebase

* Fix a huge memory issue due to expo/expo#24894

* Fix last frame flash

* Fix thum dim calculation for tall images
This commit is contained in:
dan
2024-11-09 22:34:46 +00:00
committed by GitHub
parent e73d5c6c20
commit 2d73c5a24c
12 changed files with 629 additions and 192 deletions
+8 -5
View File
@@ -1,5 +1,6 @@
import React from 'react'
import {DimensionValue, Pressable, View} from 'react-native'
import Animated, {AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
import {Image} from 'expo-image'
import {AppBskyEmbedImages} from '@atproto/api'
import {msg} from '@lingui/macro'
@@ -92,7 +93,7 @@ export function AutoSizedImage({
image: AppBskyEmbedImages.ViewImage
crop?: 'none' | 'square' | 'constrained'
hideBadge?: boolean
onPress?: () => void
onPress?: (containerRef: AnimatedRef<React.Component<{}, {}, any>>) => void
onLongPress?: () => void
onPressIn?: () => void
}) {
@@ -107,12 +108,14 @@ export function AutoSizedImage({
src: image.thumb,
knownDimensions: image.aspectRatio ?? null,
})
const containerRef = useAnimatedRef()
const cropDisabled = crop === 'none'
const isCropped = rawIsCropped && !cropDisabled
const hasAlt = !!image.alt
const contents = (
<>
<Animated.View ref={containerRef} collapsable={false}>
<Image
style={[a.w_full, a.h_full]}
source={image.thumb}
@@ -185,13 +188,13 @@ export function AutoSizedImage({
)}
</View>
) : null}
</>
</Animated.View>
)
if (cropDisabled) {
return (
<Pressable
onPress={onPress}
onPress={() => onPress?.(containerRef)}
onLongPress={onLongPress}
onPressIn={onPressIn}
// alt here is what screen readers actually use
@@ -213,7 +216,7 @@ export function AutoSizedImage({
fullBleed={crop === 'square'}
aspectRatio={constrained ?? 1}>
<Pressable
onPress={onPress}
onPress={() => onPress?.(containerRef)}
onLongPress={onLongPress}
onPressIn={onPressIn}
// alt here is what screen readers actually use
+9 -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,13 @@ 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]}
collapsable={false}>
<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
@@ -41,7 +41,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
@@ -53,8 +53,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}]}>
@@ -62,6 +68,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
{...props}
index={0}
insetBorderStyle={noCorners(['topRight', 'bottomRight'])}
containerRefs={containerRefs}
/>
</View>
<View style={[a.flex_1, {aspectRatio: 1}]}>
@@ -69,12 +76,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, {aspectRatio: 1}]}>
@@ -82,6 +92,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
{...props}
index={0}
insetBorderStyle={noCorners(['topRight', 'bottomRight'])}
containerRefs={containerRefs}
/>
</View>
<View style={[a.flex_1, {aspectRatio: 1}, gap]}>
@@ -94,6 +105,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
'bottomLeft',
'bottomRight',
])}
containerRefs={containerRefs}
/>
</View>
<View style={[a.flex_1]}>
@@ -105,13 +117,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]}>
@@ -124,6 +144,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
'topRight',
'bottomRight',
])}
containerRefs={containerRefs}
/>
</View>
<View style={[a.flex_1, {aspectRatio: 1.5}]}>
@@ -135,6 +156,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
'bottomLeft',
'bottomRight',
])}
containerRefs={containerRefs}
/>
</View>
</View>
@@ -148,6 +170,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
'topRight',
'bottomRight',
])}
containerRefs={containerRefs}
/>
</View>
<View style={[a.flex_1, {aspectRatio: 1.5}]}>
@@ -159,11 +182,13 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
'bottomLeft',
'topRight',
])}
containerRefs={containerRefs}
/>
</View>
</View>
</>
)
}
default:
return null
+10 -12
View File
@@ -6,13 +6,12 @@ import {
View,
ViewStyle,
} from 'react-native'
import Animated, {
import {
AnimatedRef,
measure,
MeasuredDimensions,
runOnJS,
runOnUI,
useAnimatedRef,
} from 'react-native-reanimated'
import {Image} from 'expo-image'
import {
@@ -69,7 +68,6 @@ export function PostEmbeds({
viewContext?: PostEmbedViewContext
}) {
const {openLightbox} = useLightboxControls()
const containerRef = useAnimatedRef()
// quote post with media
// =
@@ -149,25 +147,25 @@ export function PostEmbeds({
}))
const _openLightbox = (
index: number,
thumbDims: MeasuredDimensions | null,
thumbRects: (MeasuredDimensions | null)[],
) => {
openLightbox({
images: items.map(item => ({
images: items.map((item, i) => ({
...item,
thumbRect: thumbRects[i] ?? null,
type: 'image',
})),
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) => {
@@ -180,7 +178,7 @@ export function PostEmbeds({
const image = images[0]
return (
<ContentHider modui={moderation?.ui('contentMedia')}>
<Animated.View ref={containerRef} style={[a.mt_sm, style]}>
<View style={[a.mt_sm, style]}>
<AutoSizedImage
crop={
viewContext === PostEmbedViewContext.ThreadHighlighted
@@ -191,13 +189,13 @@ export function PostEmbeds({
: 'constrained'
}
image={image}
onPress={() => onPress(0, containerRef)}
onPress={containerRef => onPress(0, [containerRef])}
onPressIn={() => onPressIn(0)}
hideBadge={
viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
}
/>
</Animated.View>
</View>
</ContentHider>
)
}