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 PORTRAIT_UP = ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||||
const PIXEL_RATIO = PixelRatio.get()
|
const PIXEL_RATIO = PixelRatio.get()
|
||||||
|
|
||||||
|
function lockOrientationToPortrait() {
|
||||||
|
void ScreenOrientation.lockAsync(PORTRAIT_UP)
|
||||||
|
}
|
||||||
|
|
||||||
const SLOW_SPRING: WithSpringConfig = {
|
const SLOW_SPRING: WithSpringConfig = {
|
||||||
mass: IS_IOS ? 1.25 : 0.75,
|
mass: IS_IOS ? 1.25 : 0.75,
|
||||||
damping: 300,
|
damping: 300,
|
||||||
@@ -137,23 +141,38 @@ export default function ImageViewRoot({
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Delay the unlock until after we've finished the scale up animation.
|
// 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(
|
useAnimatedReaction(
|
||||||
() => openProgress.get() === 1,
|
() => openProgress.get() === 1,
|
||||||
(isOpen, wasOpen) => {
|
(isOpen, wasOpen) => {
|
||||||
if (isOpen && !wasOpen) {
|
if (isOpen && !wasOpen) {
|
||||||
runOnJS(ScreenOrientation.unlockAsync)()
|
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(() => {
|
const onFlyAway = useCallback(() => {
|
||||||
'worklet'
|
'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)
|
openProgress.set(0)
|
||||||
runOnJS(onRequestClose)()
|
runOnJS(onRequestClose)()
|
||||||
|
runOnJS(lockOrientationToPortrait)()
|
||||||
}, [onRequestClose, openProgress])
|
}, [onRequestClose, openProgress])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -178,7 +197,7 @@ export default function ImageViewRoot({
|
|||||||
key={activeLightbox.id + '-' + orientation}
|
key={activeLightbox.id + '-' + orientation}
|
||||||
lightbox={activeLightbox}
|
lightbox={activeLightbox}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
onRequestClose={onRequestClose}
|
onRequestClose={lockAndClose}
|
||||||
onPressSave={onPressSave}
|
onPressSave={onPressSave}
|
||||||
onPressShare={onPressShare}
|
onPressShare={onPressShare}
|
||||||
onFlyAway={onFlyAway}
|
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 {RefreshControl, type ViewToken} from 'react-native'
|
||||||
import {
|
import {
|
||||||
type FlatListPropsWithLayout,
|
type FlatListPropsWithLayout,
|
||||||
@@ -60,9 +67,12 @@ let List = forwardRef<ListMethods, ListProps>(
|
|||||||
ref,
|
ref,
|
||||||
): React.ReactElement<any> => {
|
): React.ReactElement<any> => {
|
||||||
const isScrolledDown = useSharedValue(false)
|
const isScrolledDown = useSharedValue(false)
|
||||||
|
const scrollOffsetY = useSharedValue(0)
|
||||||
|
const localRef = useRef<FlatList_INTERNAL | null>(null)
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const dedupe = useDedupe(400)
|
const dedupe = useDedupe(400)
|
||||||
const scrollsToTop = useAllowScrollToTop()
|
const scrollsToTop = useAllowScrollToTop()
|
||||||
|
useSaveScrollOnLightbox(localRef, scrollOffsetY)
|
||||||
|
|
||||||
const handleScrolledDownChange = useNonReactiveCallback(
|
const handleScrolledDownChange = useNonReactiveCallback(
|
||||||
(didScrollDown: boolean) => {
|
(didScrollDown: boolean) => {
|
||||||
@@ -88,6 +98,7 @@ let List = forwardRef<ListMethods, ListProps>(
|
|||||||
},
|
},
|
||||||
onScroll(e, ctx) {
|
onScroll(e, ctx) {
|
||||||
onScrollFromContext?.(e, ctx)
|
onScrollFromContext?.(e, ctx)
|
||||||
|
scrollOffsetY.set(e.contentOffset.y)
|
||||||
|
|
||||||
const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
|
const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
|
||||||
if (isScrolledDown.get() !== didScrollDown) {
|
if (isScrolledDown.get() !== didScrollDown) {
|
||||||
@@ -174,8 +185,11 @@ let List = forwardRef<ListMethods, ListProps>(
|
|||||||
scrollsToTop={scrollsToTop}
|
scrollsToTop={scrollsToTop}
|
||||||
scrollEventThrottle={1}
|
scrollEventThrottle={1}
|
||||||
style={style}
|
style={style}
|
||||||
// @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn
|
ref={node => {
|
||||||
ref={ref}
|
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()
|
const {activeLightbox} = useLightbox()
|
||||||
return useDeferredValue(!activeLightbox)
|
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