Make scroll handling contextual (#2200)
* Add an intermediate List component * Fix type * Add onScrolledDownChange * Port pager to use onScrolledDownChange * Fix on mobile * Don't pass down onScroll (replacement TBD) * Remove resetMainScroll * Replace onMainScroll with MainScrollProvider * Hook ScrollProvider to pager * Fix the remaining special case * Optimize a bit * Enforce that onScroll cannot be passed * Keep value updated even if no handler * Also memo it
This commit is contained in:
parent
fa3ccafa80
commit
7fd7970237
26 changed files with 280 additions and 354 deletions
64
src/view/com/util/List.tsx
Normal file
64
src/view/com/util/List.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import React, {memo, startTransition} from 'react'
|
||||
import {FlatListProps} from 'react-native'
|
||||
import {FlatList_INTERNAL} from './Views'
|
||||
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||
import {runOnJS, useSharedValue} from 'react-native-reanimated'
|
||||
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||
|
||||
export type ListMethods = FlatList_INTERNAL
|
||||
export type ListProps<ItemT> = Omit<
|
||||
FlatListProps<ItemT>,
|
||||
'onScroll' // Use ScrollContext instead.
|
||||
> & {
|
||||
onScrolledDownChange?: (isScrolledDown: boolean) => void
|
||||
}
|
||||
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
|
||||
|
||||
const SCROLLED_DOWN_LIMIT = 200
|
||||
|
||||
function ListImpl<ItemT>(
|
||||
{onScrolledDownChange, ...props}: ListProps<ItemT>,
|
||||
ref: React.Ref<ListMethods>,
|
||||
) {
|
||||
const isScrolledDown = useSharedValue(false)
|
||||
const contextScrollHandlers = useScrollHandlers()
|
||||
|
||||
function handleScrolledDownChange(didScrollDown: boolean) {
|
||||
startTransition(() => {
|
||||
onScrolledDownChange?.(didScrollDown)
|
||||
})
|
||||
}
|
||||
|
||||
const scrollHandler = useAnimatedScrollHandler({
|
||||
onBeginDrag(e, ctx) {
|
||||
contextScrollHandlers.onBeginDrag?.(e, ctx)
|
||||
},
|
||||
onEndDrag(e, ctx) {
|
||||
contextScrollHandlers.onEndDrag?.(e, ctx)
|
||||
},
|
||||
onScroll(e, ctx) {
|
||||
contextScrollHandlers.onScroll?.(e, ctx)
|
||||
|
||||
const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
|
||||
if (isScrolledDown.value !== didScrollDown) {
|
||||
isScrolledDown.value = didScrollDown
|
||||
if (onScrolledDownChange != null) {
|
||||
runOnJS(handleScrolledDownChange)(didScrollDown)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<FlatList_INTERNAL
|
||||
{...props}
|
||||
onScroll={scrollHandler}
|
||||
scrollEventThrottle={1}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
|
||||
props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>},
|
||||
) => React.ReactElement
|
97
src/view/com/util/MainScrollProvider.tsx
Normal file
97
src/view/com/util/MainScrollProvider.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
import React, {useCallback} from 'react'
|
||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||
import {NativeScrollEvent} from 'react-native'
|
||||
import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useSharedValue, interpolate} from 'react-native-reanimated'
|
||||
|
||||
function clamp(num: number, min: number, max: number) {
|
||||
'worklet'
|
||||
return Math.min(Math.max(num, min), max)
|
||||
}
|
||||
|
||||
export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||
const {headerHeight} = useShellLayout()
|
||||
const mode = useMinimalShellMode()
|
||||
const setMode = useSetMinimalShellMode()
|
||||
const startDragOffset = useSharedValue<number | null>(null)
|
||||
const startMode = useSharedValue<number | null>(null)
|
||||
|
||||
const onBeginDrag = useCallback(
|
||||
(e: NativeScrollEvent) => {
|
||||
'worklet'
|
||||
startDragOffset.value = e.contentOffset.y
|
||||
startMode.value = mode.value
|
||||
},
|
||||
[mode, startDragOffset, startMode],
|
||||
)
|
||||
|
||||
const onEndDrag = useCallback(
|
||||
(e: NativeScrollEvent) => {
|
||||
'worklet'
|
||||
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)
|
||||
}
|
||||
},
|
||||
[startDragOffset, startMode, setMode, mode, headerHeight],
|
||||
)
|
||||
|
||||
const onScroll = useCallback(
|
||||
(e: NativeScrollEvent) => {
|
||||
'worklet'
|
||||
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
|
||||
}
|
||||
},
|
||||
[headerHeight, mode, setMode, startDragOffset, startMode],
|
||||
)
|
||||
|
||||
return (
|
||||
<ScrollProvider
|
||||
onBeginDrag={onBeginDrag}
|
||||
onEndDrag={onEndDrag}
|
||||
onScroll={onScroll}>
|
||||
{children}
|
||||
</ScrollProvider>
|
||||
)
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
import React, {useEffect, useState} from 'react'
|
||||
import {
|
||||
NativeSyntheticEvent,
|
||||
NativeScrollEvent,
|
||||
Pressable,
|
||||
RefreshControl,
|
||||
StyleSheet,
|
||||
View,
|
||||
ScrollView,
|
||||
} from 'react-native'
|
||||
import {FlatList} from './Views'
|
||||
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
||||
import {FlatList_INTERNAL} from './Views'
|
||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||
import {Text} from './text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
|
@ -38,7 +39,7 @@ export const ViewSelector = React.forwardRef<
|
|||
| null
|
||||
| undefined
|
||||
onSelectView?: (viewIndex: number) => void
|
||||
onScroll?: OnScrollCb
|
||||
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
|
||||
onRefresh?: () => void
|
||||
onEndReached?: (info: {distanceFromEnd: number}) => void
|
||||
}
|
||||
|
@ -59,7 +60,7 @@ export const ViewSelector = React.forwardRef<
|
|||
) {
|
||||
const pal = usePalette('default')
|
||||
const [selectedIndex, setSelectedIndex] = useState<number>(0)
|
||||
const flatListRef = React.useRef<FlatList>(null)
|
||||
const flatListRef = React.useRef<FlatList_INTERNAL>(null)
|
||||
|
||||
// events
|
||||
// =
|
||||
|
@ -110,7 +111,7 @@ export const ViewSelector = React.forwardRef<
|
|||
[items],
|
||||
)
|
||||
return (
|
||||
<FlatList
|
||||
<FlatList_INTERNAL
|
||||
ref={flatListRef}
|
||||
data={data}
|
||||
keyExtractor={keyExtractor}
|
||||
|
|
2
src/view/com/util/Views.d.ts
vendored
2
src/view/com/util/Views.d.ts
vendored
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import {ViewProps} from 'react-native'
|
||||
export {FlatList, ScrollView} from 'react-native'
|
||||
export {FlatList as FlatList_INTERNAL, ScrollView} from 'react-native'
|
||||
export function CenteredView({
|
||||
style,
|
||||
sideBorders,
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import {View} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
|
||||
export const FlatList = Animated.FlatList
|
||||
export const FlatList_INTERNAL = Animated.FlatList
|
||||
export const ScrollView = Animated.ScrollView
|
||||
export function CenteredView(props) {
|
||||
return <View {...props} />
|
||||
|
|
|
@ -49,7 +49,7 @@ export function CenteredView({
|
|||
return <View style={style} {...props} />
|
||||
}
|
||||
|
||||
export const FlatList = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
{
|
||||
contentContainerStyle,
|
||||
style,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue