Sync top/bottom bar disappearance to the scroll (#1855)
* Disable existing code that toggles shell * Make shell mode a float * Translate based on the gesture * Track header and footer heights * Add web support * Fix types and cleanup * Add back isScrolled logic * Add comments
This commit is contained in:
parent
1dcf882619
commit
7a55ca6133
10 changed files with 183 additions and 107 deletions
|
@ -1,45 +1,29 @@
|
|||
import {
|
||||
AnimatableValue,
|
||||
interpolate,
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
Easing,
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
import {interpolate, useAnimatedStyle} from 'react-native-reanimated'
|
||||
import {useMinimalShellMode as useMinimalShellModeState} from '#/state/shell/minimal-mode'
|
||||
|
||||
function withShellTiming<T extends AnimatableValue>(value: T): T {
|
||||
'worklet'
|
||||
return withTiming(value, {
|
||||
duration: 125,
|
||||
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
|
||||
})
|
||||
}
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
|
||||
export function useMinimalShellMode() {
|
||||
const mode = useMinimalShellModeState()
|
||||
const {footerHeight, headerHeight} = useShellLayout()
|
||||
|
||||
const footerMinimalShellTransform = useAnimatedStyle(() => {
|
||||
return {
|
||||
pointerEvents: mode.value ? 'none' : 'auto',
|
||||
opacity: withShellTiming(interpolate(mode.value ? 1 : 0, [0, 1], [1, 0])),
|
||||
pointerEvents: mode.value === 0 ? 'auto' : 'none',
|
||||
opacity: Math.pow(1 - mode.value, 2),
|
||||
transform: [
|
||||
{
|
||||
translateY: withShellTiming(
|
||||
interpolate(mode.value ? 1 : 0, [0, 1], [0, 25]),
|
||||
),
|
||||
translateY: interpolate(mode.value, [0, 1], [0, footerHeight.value]),
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
const headerMinimalShellTransform = useAnimatedStyle(() => {
|
||||
return {
|
||||
pointerEvents: mode.value ? 'none' : 'auto',
|
||||
opacity: withShellTiming(interpolate(mode.value ? 1 : 0, [0, 1], [1, 0])),
|
||||
pointerEvents: mode.value === 0 ? 'auto' : 'none',
|
||||
opacity: Math.pow(1 - mode.value, 2),
|
||||
transform: [
|
||||
{
|
||||
translateY: withShellTiming(
|
||||
interpolate(mode.value ? 1 : 0, [0, 1], [0, -25]),
|
||||
),
|
||||
translateY: interpolate(mode.value, [0, 1], [0, -headerHeight.value]),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@ -48,9 +32,7 @@ export function useMinimalShellMode() {
|
|||
return {
|
||||
transform: [
|
||||
{
|
||||
translateY: withShellTiming(
|
||||
interpolate(mode.value ? 1 : 0, [0, 1], [-44, 0]),
|
||||
),
|
||||
translateY: interpolate(mode.value, [0, 1], [-44, 0]),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import {useState, useCallback, useRef} from 'react'
|
||||
import {useState, useCallback} from 'react'
|
||||
import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native'
|
||||
import {s} from 'lib/styles'
|
||||
import {useWebMediaQueries} from './useWebMediaQueries'
|
||||
import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
import {s} from 'lib/styles'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {
|
||||
useAnimatedScrollHandler,
|
||||
useSharedValue,
|
||||
interpolate,
|
||||
runOnJS,
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
const Y_LIMIT = 10
|
||||
|
||||
const useDeviceLimits = () => {
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
return {
|
||||
dyLimitUp: isDesktop ? 30 : 10,
|
||||
dyLimitDown: isDesktop ? 150 : 10,
|
||||
}
|
||||
function clamp(num: number, min: number, max: number) {
|
||||
'worklet'
|
||||
return Math.min(Math.max(num, min), max)
|
||||
}
|
||||
|
||||
export type OnScrollCb = (
|
||||
|
@ -20,53 +22,82 @@ export type OnScrollCb = (
|
|||
export type ResetCb = () => void
|
||||
|
||||
export function useOnMainScroll(): [OnScrollCb, boolean, ResetCb] {
|
||||
let lastY = useRef(0)
|
||||
let [isScrolledDown, setIsScrolledDown] = useState(false)
|
||||
const {dyLimitUp, dyLimitDown} = useDeviceLimits()
|
||||
const minimalShellMode = useMinimalShellMode()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {headerHeight} = useShellLayout()
|
||||
const [isScrolledDown, setIsScrolledDown] = useState(false)
|
||||
const mode = useMinimalShellMode()
|
||||
const setMode = useSetMinimalShellMode()
|
||||
const startDragOffset = useSharedValue<number | null>(null)
|
||||
const startMode = useSharedValue<number | null>(null)
|
||||
|
||||
const scrollHandler = useAnimatedScrollHandler({
|
||||
onBeginDrag(e) {
|
||||
startDragOffset.value = e.contentOffset.y
|
||||
startMode.value = mode.value
|
||||
},
|
||||
onEndDrag(e) {
|
||||
startDragOffset.value = null
|
||||
startMode.value = null
|
||||
if (e.contentOffset.y < headerHeight.value / 2) {
|
||||
// If we're close to the top, show the shell.
|
||||
setMode(false)
|
||||
} else {
|
||||
// Snap to whichever state is the closest.
|
||||
setMode(Math.round(mode.value) === 1)
|
||||
}
|
||||
},
|
||||
onScroll(e) {
|
||||
// Keep track of whether we want to show "scroll to top".
|
||||
if (!isScrolledDown && e.contentOffset.y > s.window.height) {
|
||||
runOnJS(setIsScrolledDown)(true)
|
||||
} else if (isScrolledDown && e.contentOffset.y < s.window.height) {
|
||||
runOnJS(setIsScrolledDown)(false)
|
||||
}
|
||||
|
||||
if (startDragOffset.value === null || startMode.value === null) {
|
||||
if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) {
|
||||
// If we're close enough to the top, always show the shell.
|
||||
// Even if we're not dragging.
|
||||
setMode(false)
|
||||
return
|
||||
}
|
||||
if (isWeb) {
|
||||
// On the web, there is no concept of "starting" the drag.
|
||||
// When we get the first scroll event, we consider that the start.
|
||||
startDragOffset.value = e.contentOffset.y
|
||||
startMode.value = mode.value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// The "mode" value is always between 0 and 1.
|
||||
// Figure out how much to move it based on the current dragged distance.
|
||||
const dy = e.contentOffset.y - startDragOffset.value
|
||||
const dProgress = interpolate(
|
||||
dy,
|
||||
[-headerHeight.value, headerHeight.value],
|
||||
[-1, 1],
|
||||
)
|
||||
const newValue = clamp(startMode.value + dProgress, 0, 1)
|
||||
if (newValue !== mode.value) {
|
||||
// Manually adjust the value. This won't be (and shouldn't be) animated.
|
||||
mode.value = newValue
|
||||
}
|
||||
if (isWeb) {
|
||||
// On the web, there is no concept of "starting" the drag,
|
||||
// so we don't have any specific anchor point to calculate the distance.
|
||||
// Instead, update it continuosly along the way and diff with the last event.
|
||||
startDragOffset.value = e.contentOffset.y
|
||||
startMode.value = mode.value
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return [
|
||||
useCallback(
|
||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const y = event.nativeEvent.contentOffset.y
|
||||
const dy = y - (lastY.current || 0)
|
||||
lastY.current = y
|
||||
|
||||
if (!minimalShellMode.value && dy > dyLimitDown && y > Y_LIMIT) {
|
||||
setMinimalShellMode(true)
|
||||
} else if (
|
||||
minimalShellMode.value &&
|
||||
(dy < dyLimitUp * -1 || y <= Y_LIMIT)
|
||||
) {
|
||||
setMinimalShellMode(false)
|
||||
}
|
||||
|
||||
if (
|
||||
!isScrolledDown &&
|
||||
event.nativeEvent.contentOffset.y > s.window.height
|
||||
) {
|
||||
setIsScrolledDown(true)
|
||||
} else if (
|
||||
isScrolledDown &&
|
||||
event.nativeEvent.contentOffset.y < s.window.height
|
||||
) {
|
||||
setIsScrolledDown(false)
|
||||
}
|
||||
},
|
||||
[
|
||||
dyLimitDown,
|
||||
dyLimitUp,
|
||||
isScrolledDown,
|
||||
minimalShellMode,
|
||||
setMinimalShellMode,
|
||||
],
|
||||
),
|
||||
scrollHandler,
|
||||
isScrolledDown,
|
||||
useCallback(() => {
|
||||
setIsScrolledDown(false)
|
||||
setMinimalShellMode(false)
|
||||
lastY.current = 1e8 // NOTE we set this very high so that the onScroll logic works right -prf
|
||||
}, [setIsScrolledDown, setMinimalShellMode]),
|
||||
setMode(false)
|
||||
}, [setMode]),
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue