refactor lightbox orientation transform
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user