Lazily measure lightbox thumbnails (#10270)
This commit is contained in:
@@ -23,8 +23,10 @@ import Animated, {
|
||||
cancelAnimation,
|
||||
interpolate,
|
||||
measure,
|
||||
type MeasuredDimensions,
|
||||
ReduceMotion,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
type SharedValue,
|
||||
useAnimatedReaction,
|
||||
useAnimatedRef,
|
||||
@@ -73,12 +75,11 @@ const FAST_SPRING: WithSpringConfig = {
|
||||
}
|
||||
|
||||
function canAnimate(lightbox: Lightbox): boolean {
|
||||
return (
|
||||
!PlatformInfo.getIsReducedMotionEnabled() &&
|
||||
lightbox.images.every(
|
||||
img => img.thumbRect && (img.dimensions || img.thumbDimensions),
|
||||
)
|
||||
)
|
||||
if (PlatformInfo.getIsReducedMotionEnabled()) {
|
||||
return false
|
||||
}
|
||||
const img = lightbox.images[lightbox.index]
|
||||
return !!img.thumbRect && !!(img.dimensions || img.thumbDimensions)
|
||||
}
|
||||
|
||||
export default function ImageViewRoot({
|
||||
@@ -99,6 +100,9 @@ export default function ImageViewRoot({
|
||||
'portrait',
|
||||
)
|
||||
const openProgress = useSharedValue(0)
|
||||
const thumbRects = useSharedValue<Record<number, MeasuredDimensions | null>>(
|
||||
{},
|
||||
)
|
||||
|
||||
if (!activeLightbox && nextLightbox) {
|
||||
setActiveLightbox(nextLightbox)
|
||||
@@ -109,6 +113,12 @@ export default function ImageViewRoot({
|
||||
return
|
||||
}
|
||||
|
||||
const initial: Record<number, MeasuredDimensions | null> = {}
|
||||
nextLightbox.images.forEach((img, i) => {
|
||||
initial[i] = img.thumbRect ?? null
|
||||
})
|
||||
thumbRects.set(initial)
|
||||
|
||||
const isAnimated = canAnimate(nextLightbox)
|
||||
|
||||
// https://github.com/software-mansion/react-native-reanimated/issues/6677
|
||||
@@ -125,13 +135,21 @@ export default function ImageViewRoot({
|
||||
)
|
||||
})
|
||||
}
|
||||
}, [nextLightbox, openProgress])
|
||||
}, [nextLightbox, openProgress, thumbRects])
|
||||
|
||||
const onFullyClosed = useCallback(() => {
|
||||
setActiveLightbox(null)
|
||||
runOnUI(() => {
|
||||
'worklet'
|
||||
thumbRects.set({})
|
||||
})()
|
||||
}, [thumbRects])
|
||||
|
||||
useAnimatedReaction(
|
||||
() => openProgress.get() === 0,
|
||||
(isGone, wasGone) => {
|
||||
if (isGone && !wasGone) {
|
||||
runOnJS(setActiveLightbox)(null)
|
||||
runOnJS(onFullyClosed)()
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -184,6 +202,7 @@ export default function ImageViewRoot({
|
||||
onFlyAway={onFlyAway}
|
||||
safeAreaRef={ref}
|
||||
openProgress={openProgress}
|
||||
thumbRects={thumbRects}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
@@ -200,6 +219,7 @@ function ImageView({
|
||||
onFlyAway,
|
||||
safeAreaRef,
|
||||
openProgress,
|
||||
thumbRects,
|
||||
}: {
|
||||
lightbox: Lightbox
|
||||
orientation: 'portrait' | 'landscape'
|
||||
@@ -209,6 +229,7 @@ function ImageView({
|
||||
onFlyAway: () => void
|
||||
safeAreaRef: AnimatedRef<View>
|
||||
openProgress: SharedValue<number>
|
||||
thumbRects: SharedValue<Record<number, MeasuredDimensions | null>>
|
||||
}) {
|
||||
const {images, index: initialImageIndex} = lightbox
|
||||
const isAnimated = useMemo(() => canAnimate(lightbox), [lightbox])
|
||||
@@ -216,7 +237,7 @@ function ImageView({
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [imageIndex, setImageIndex] = useState(initialImageIndex)
|
||||
const [showControls, setShowControls] = useState(true)
|
||||
const [isAltExpanded, setAltExpanded] = useState(false)
|
||||
const [isAltExpanded, setIsAltExpanded] = useState(false)
|
||||
const dismissSwipeTranslateY = useSharedValue(0)
|
||||
const isFlyingAway = useSharedValue(false)
|
||||
|
||||
@@ -287,6 +308,24 @@ function ImageView({
|
||||
}
|
||||
})
|
||||
|
||||
const handleRequestClose = useCallback(() => {
|
||||
const activeRef = images[imageIndex]?.thumbRef
|
||||
if (isAnimated && activeRef) {
|
||||
runOnUI(() => {
|
||||
'worklet'
|
||||
const rect = measure(activeRef)
|
||||
thumbRects.modify(rects => {
|
||||
'worklet'
|
||||
rects[imageIndex] = rect
|
||||
return rects
|
||||
})
|
||||
runOnJS(onRequestClose)()
|
||||
})()
|
||||
} else {
|
||||
onRequestClose()
|
||||
}
|
||||
}, [isAnimated, images, imageIndex, thumbRects, onRequestClose])
|
||||
|
||||
const onTap = useCallback(() => {
|
||||
setShowControls(show => !show)
|
||||
}, [])
|
||||
@@ -355,7 +394,7 @@ function ImageView({
|
||||
onTap={onTap}
|
||||
onZoom={onZoom}
|
||||
imageSrc={imageSrc}
|
||||
onRequestClose={onRequestClose}
|
||||
onRequestClose={handleRequestClose}
|
||||
isScrollViewBeingDragged={isDragging}
|
||||
showControls={showControls}
|
||||
safeAreaRef={safeAreaRef}
|
||||
@@ -364,6 +403,8 @@ function ImageView({
|
||||
isActive={i === imageIndex}
|
||||
dismissSwipeTranslateY={dismissSwipeTranslateY}
|
||||
openProgress={openProgress}
|
||||
thumbRects={thumbRects}
|
||||
imageIndex={i}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
@@ -372,7 +413,7 @@ function ImageView({
|
||||
<Animated.View
|
||||
style={animatedHeaderStyle}
|
||||
renderToHardwareTextureAndroid>
|
||||
<ImageDefaultHeader onRequestClose={onRequestClose} />
|
||||
<ImageDefaultHeader onRequestClose={handleRequestClose} />
|
||||
</Animated.View>
|
||||
<Animated.View
|
||||
style={animatedFooterStyle}
|
||||
@@ -381,7 +422,7 @@ function ImageView({
|
||||
images={images}
|
||||
index={imageIndex}
|
||||
isAltExpanded={isAltExpanded}
|
||||
toggleAltExpanded={() => setAltExpanded(e => !e)}
|
||||
toggleAltExpanded={() => setIsAltExpanded(e => !e)}
|
||||
onPressSave={onPressSave}
|
||||
onPressShare={onPressShare}
|
||||
/>
|
||||
@@ -404,6 +445,8 @@ function LightboxImage({
|
||||
safeAreaRef,
|
||||
openProgress,
|
||||
dismissSwipeTranslateY,
|
||||
thumbRects,
|
||||
imageIndex,
|
||||
}: {
|
||||
imageSrc: ImageSource
|
||||
onRequestClose: () => void
|
||||
@@ -417,6 +460,8 @@ function LightboxImage({
|
||||
safeAreaRef: AnimatedRef<View>
|
||||
openProgress: SharedValue<number>
|
||||
dismissSwipeTranslateY: SharedValue<number>
|
||||
thumbRects: SharedValue<Record<number, MeasuredDimensions | null>>
|
||||
imageIndex: number
|
||||
}) {
|
||||
const [fetchedDims, setFetchedDims] = useState<Dimensions | null>(null)
|
||||
const dims = fetchedDims ?? imageSrc.dimensions ?? imageSrc.thumbDimensions
|
||||
@@ -449,7 +494,7 @@ function LightboxImage({
|
||||
return safeArea
|
||||
}, [safeAreaRef, heightDelayedForJSThreadOnly, widthDelayedForJSThreadOnly])
|
||||
|
||||
const {thumbRect} = imageSrc
|
||||
const {thumbRect: thumbRectJS} = imageSrc
|
||||
const transforms = useDerivedValue(() => {
|
||||
'worklet'
|
||||
const safeArea = measureSafeArea()
|
||||
@@ -467,13 +512,21 @@ function LightboxImage({
|
||||
}
|
||||
}
|
||||
|
||||
if (isActive && thumbRect && imageAspect && openProgressValue < 1) {
|
||||
return interpolateTransform(
|
||||
openProgressValue,
|
||||
thumbRect,
|
||||
safeArea,
|
||||
imageAspect,
|
||||
)
|
||||
if (isActive && imageAspect && openProgressValue < 1) {
|
||||
let thumbRect
|
||||
if (_WORKLET) {
|
||||
thumbRect = thumbRects.get()[imageIndex]
|
||||
} else {
|
||||
thumbRect = thumbRectJS
|
||||
}
|
||||
if (thumbRect) {
|
||||
return interpolateTransform(
|
||||
openProgressValue,
|
||||
thumbRect,
|
||||
safeArea,
|
||||
imageAspect,
|
||||
)
|
||||
}
|
||||
}
|
||||
return {
|
||||
isHidden: false,
|
||||
|
||||
Reference in New Issue
Block a user