refactor lightbox orientation transform

This commit is contained in:
vineyardbovines
2026-04-02 13:04:38 -04:00
parent eb566c5fcc
commit c051bda3db
2 changed files with 64 additions and 8 deletions
+24 -5
View File
@@ -59,6 +59,10 @@ type Rect = {x: number; y: number; width: number; height: number}
const PORTRAIT_UP = ScreenOrientation.OrientationLock.PORTRAIT_UP
const PIXEL_RATIO = PixelRatio.get()
function lockOrientationToPortrait() {
void ScreenOrientation.lockAsync(PORTRAIT_UP)
}
const SLOW_SPRING: WithSpringConfig = {
mass: IS_IOS ? 1.25 : 0.75,
damping: 300,
@@ -137,23 +141,38 @@ export default function ImageViewRoot({
)
// Delay the unlock until after we've finished the scale up animation.
// It's complicated to do the same for locking it back so we don't attempt that.
useAnimatedReaction(
() => openProgress.get() === 1,
(isOpen, wasOpen) => {
if (isOpen && !wasOpen) {
runOnJS(ScreenOrientation.unlockAsync)()
} else if (!isOpen && wasOpen) {
// default is PORTRAIT_UP - set via config plugin in app.config.js -sfn
runOnJS(ScreenOrientation.lockAsync)(PORTRAIT_UP)
}
},
)
// Lock orientation back to portrait BEFORE triggering the close animation.
// This ensures the feed re-layouts while the lightbox is still fully opaque,
// preventing the scroll-to-top issue caused by orientation change.
const lockAndClose = useCallback(() => {
if (orientation === 'landscape') {
void ScreenOrientation.lockAsync(PORTRAIT_UP).then(() => {
// Give the layout time to settle behind the lightbox.
setTimeout(onRequestClose, 500)
})
} else {
void ScreenOrientation.lockAsync(PORTRAIT_UP)
onRequestClose()
}
}, [orientation, onRequestClose])
const onFlyAway = useCallback(() => {
'worklet'
// Fly-away: image is already off-screen, dismiss immediately.
// Lock orientation after — the fast swipe makes any brief scroll
// glitch less noticeable than the close button path.
openProgress.set(0)
runOnJS(onRequestClose)()
runOnJS(lockOrientationToPortrait)()
}, [onRequestClose, openProgress])
return (
@@ -178,7 +197,7 @@ export default function ImageViewRoot({
key={activeLightbox.id + '-' + orientation}
lightbox={activeLightbox}
orientation={orientation}
onRequestClose={onRequestClose}
onRequestClose={lockAndClose}
onPressSave={onPressSave}
onPressShare={onPressShare}
onFlyAway={onFlyAway}
+40 -3
View File
@@ -1,4 +1,11 @@
import {forwardRef, memo, useDeferredValue, useMemo} from 'react'
import {
forwardRef,
memo,
useDeferredValue,
useEffect,
useMemo,
useRef,
} from 'react'
import {RefreshControl, type ViewToken} from 'react-native'
import {
type FlatListPropsWithLayout,
@@ -60,9 +67,12 @@ let List = forwardRef<ListMethods, ListProps>(
ref,
): React.ReactElement<any> => {
const isScrolledDown = useSharedValue(false)
const scrollOffsetY = useSharedValue(0)
const localRef = useRef<FlatList_INTERNAL | null>(null)
const t = useTheme()
const dedupe = useDedupe(400)
const scrollsToTop = useAllowScrollToTop()
useSaveScrollOnLightbox(localRef, scrollOffsetY)
const handleScrolledDownChange = useNonReactiveCallback(
(didScrollDown: boolean) => {
@@ -88,6 +98,7 @@ let List = forwardRef<ListMethods, ListProps>(
},
onScroll(e, ctx) {
onScrollFromContext?.(e, ctx)
scrollOffsetY.set(e.contentOffset.y)
const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
if (isScrolledDown.get() !== didScrollDown) {
@@ -174,8 +185,11 @@ let List = forwardRef<ListMethods, ListProps>(
scrollsToTop={scrollsToTop}
scrollEventThrottle={1}
style={style}
// @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn
ref={ref}
ref={node => {
localRef.current = node
if (typeof ref === 'function') ref(node)
else if (ref) (ref as React.MutableRefObject<any>).current = node
}}
/>
)
},
@@ -192,3 +206,26 @@ function useAllowScrollToTopIOS() {
const {activeLightbox} = useLightbox()
return useDeferredValue(!activeLightbox)
}
// Save scroll position when lightbox opens, restore it when it closes.
// This prevents the scroll-to-top caused by orientation changes during lightbox viewing.
function useSaveScrollOnLightbox(
listRef: React.RefObject<FlatList_INTERNAL | null>,
scrollOffsetY: {get(): number},
) {
const {activeLightbox} = useLightbox()
const savedOffset = useRef<number | null>(null)
useEffect(() => {
if (activeLightbox) {
savedOffset.current = scrollOffsetY.get()
} else if (savedOffset.current !== null) {
const offset = savedOffset.current
savedOffset.current = null
// Restore after layout settles from any orientation change.
requestAnimationFrame(() => {
listRef.current?.scrollToOffset({offset, animated: false})
})
}
}, [activeLightbox, listRef, scrollOffsetY])
}