diff options
Diffstat (limited to 'src/view/com/util')
-rw-r--r-- | src/view/com/util/List.tsx | 64 | ||||
-rw-r--r-- | src/view/com/util/MainScrollProvider.tsx | 97 | ||||
-rw-r--r-- | src/view/com/util/ViewSelector.tsx | 11 | ||||
-rw-r--r-- | src/view/com/util/Views.d.ts | 2 | ||||
-rw-r--r-- | src/view/com/util/Views.jsx | 2 | ||||
-rw-r--r-- | src/view/com/util/Views.web.tsx | 2 |
6 files changed, 170 insertions, 8 deletions
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx new file mode 100644 index 000000000..5947fe87a --- /dev/null +++ b/src/view/com/util/List.tsx @@ -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 diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx new file mode 100644 index 000000000..31a4ef0c8 --- /dev/null +++ b/src/view/com/util/MainScrollProvider.tsx @@ -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> + ) +} diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index 935d93033..ee993c564 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -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} diff --git a/src/view/com/util/Views.d.ts b/src/view/com/util/Views.d.ts index 91df1d6bc..6a90cc229 100644 --- a/src/view/com/util/Views.d.ts +++ b/src/view/com/util/Views.d.ts @@ -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, diff --git a/src/view/com/util/Views.jsx b/src/view/com/util/Views.jsx index 8a93ce511..7d6120583 100644 --- a/src/view/com/util/Views.jsx +++ b/src/view/com/util/Views.jsx @@ -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} /> diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx index 5a4f266fd..db3b9de0d 100644 --- a/src/view/com/util/Views.web.tsx +++ b/src/view/com/util/Views.web.tsx @@ -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, |