diff --git a/src/components/common/SceneWrapper.tsx b/src/components/common/SceneWrapper.tsx index a4fe542c20a..234f054bff5 100644 --- a/src/components/common/SceneWrapper.tsx +++ b/src/components/common/SceneWrapper.tsx @@ -119,7 +119,7 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { const headerBarHeight = getDefaultHeaderHeight(frame, false, 0) // If the scene has scroll, this will be required for tabs and/or header animation - const handleScroll = useSceneScrollHandler() + const handleScroll = useSceneScrollHandler(scroll && (hasTabs || hasHeader)) const renderScene = (safeAreaInsets: EdgeInsets, keyboardAnimation: Animated.Value | undefined, trackerValue: number): JSX.Element => { // If function children, the caller handles the insets and overscroll diff --git a/src/components/navigation/HeaderBackground.tsx b/src/components/navigation/HeaderBackground.tsx index bf9e175cf87..45a210cdd21 100644 --- a/src/components/navigation/HeaderBackground.tsx +++ b/src/components/navigation/HeaderBackground.tsx @@ -1,17 +1,22 @@ import * as React from 'react' -import { StyleSheet, View } from 'react-native' +import { StyleSheet } from 'react-native' import LinearGradient from 'react-native-linear-gradient' +import Animated, { interpolate, SharedValue, useAnimatedStyle } from 'react-native-reanimated' import { BlurView } from 'rn-id-blurview' +import { useSceneScrollContext } from '../../state/SceneScrollState' import { styled } from '../hoc/styled' import { useTheme } from '../services/ThemeContext' import { DividerLine } from '../themed/DividerLine' +import { MAX_TAB_BAR_HEIGHT } from '../themed/MenuTabs' -export const HeaderBackground = () => { +export const HeaderBackground = (props: any) => { const theme = useTheme() + const { scrollY } = useSceneScrollContext() + return ( - + @@ -19,11 +24,17 @@ export const HeaderBackground = () => { ) } -const HeaderBackgroundContainerView = styled(View)({ - ...StyleSheet.absoluteFillObject, - alignItems: 'stretch', - justifyContent: 'flex-end' -}) +const HeaderBackgroundContainerView = styled(Animated.View)<{ scrollY: SharedValue }>(() => ({ scrollY }) => [ + { + ...StyleSheet.absoluteFillObject, + alignItems: 'stretch', + justifyContent: 'flex-end', + opacity: 0 + }, + useAnimatedStyle(() => ({ + opacity: interpolate(scrollY.value, [0, MAX_TAB_BAR_HEIGHT], [0, 1]) + })) +]) const HeaderLinearGradient = styled(LinearGradient)({ flex: 1 diff --git a/src/state/SceneScrollState.tsx b/src/state/SceneScrollState.tsx index fc2fce27004..a49305944f8 100644 --- a/src/state/SceneScrollState.tsx +++ b/src/state/SceneScrollState.tsx @@ -1,70 +1,156 @@ -import { useMemo } from 'react' +import { useIsFocused } from '@react-navigation/native' +import { useCallback, useEffect, useMemo, useState } from 'react' import { NativeScrollEvent, NativeSyntheticEvent } from 'react-native' -import { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' +import { SharedValue, useAnimatedScrollHandler, useDerivedValue, useSharedValue } from 'react-native-reanimated' import { createStateProvider } from './createStateProvider' -export const [SceneScrollProvider, useSceneScrollContext] = createStateProvider(() => { +interface InternalScrollState { + dragStartX: SharedValue + dragStartY: SharedValue + scrollX: SharedValue + scrollY: SharedValue + scrollBeginEvent: SharedValue + scrollEndEvent: SharedValue + scrollMomentumBeginEvent: SharedValue + scrollMomentumEndEvent: SharedValue +} + +export interface ScrollContextValue { + scrollX: SharedValue + scrollY: SharedValue + scrollXDelta: SharedValue + scrollYDelta: SharedValue + scrollBeginEvent: SharedValue + scrollEndEvent: SharedValue + scrollMomentumBeginEvent: SharedValue + scrollMomentumEndEvent: SharedValue + updateScrollState: (state: InternalScrollState) => void +} + +export const [SceneScrollProvider, useSceneScrollContext] = createStateProvider((): ScrollContextValue => { + const dragStartX = useSharedValue(0) + const dragStartY = useSharedValue(0) const scrollX = useSharedValue(0) - const scrollXDelta = useSharedValue(0) const scrollY = useSharedValue(0) - const scrollYDelta = useSharedValue(0) const scrollBeginEvent = useSharedValue(null) const scrollEndEvent = useSharedValue(null) const scrollMomentumBeginEvent = useSharedValue(null) const scrollMomentumEndEvent = useSharedValue(null) - return useMemo( - () => ({ - scrollX, + const scrollXDelta = useDerivedValue(() => scrollX.value - dragStartX.value) + const scrollYDelta = useDerivedValue(() => scrollY.value - dragStartY.value) + + const updateScrollState = useCallback((state: InternalScrollState) => { + setScrollState(state) + }, []) + + const [scrollState, setScrollState] = useState({ + dragStartX, + dragStartY, + scrollX, + scrollY, + scrollBeginEvent, + scrollEndEvent, + scrollMomentumBeginEvent, + scrollMomentumEndEvent + }) + + return useMemo(() => { + return { + scrollX: scrollState.scrollX, + scrollY: scrollState.scrollY, + scrollBeginEvent: scrollState.scrollBeginEvent, + scrollEndEvent: scrollState.scrollEndEvent, + scrollMomentumBeginEvent: scrollState.scrollMomentumBeginEvent, + scrollMomentumEndEvent: scrollState.scrollMomentumEndEvent, scrollXDelta, - scrollY, scrollYDelta, - scrollBeginEvent, - scrollEndEvent, - scrollMomentumBeginEvent, - scrollMomentumEndEvent - }), - [scrollBeginEvent, scrollEndEvent, scrollMomentumBeginEvent, scrollMomentumEndEvent, scrollX, scrollXDelta, scrollY, scrollYDelta] - ) + updateScrollState + } + }, [scrollState, scrollXDelta, scrollYDelta, updateScrollState]) }) export type SceneScrollHandler = (event: NativeSyntheticEvent) => void -export const useSceneScrollHandler = (): SceneScrollHandler => { - const sceneScrollContext = useSceneScrollContext() +/** + * Return a Reanimated scroll handler (special worklet handler ref) to be attached + * to a animated scrollable component (Animate.ScrollView, Animate.FlatList, etc). + * + * The hook works by creating local component state of reanimated shared-values which + * are updated based on the scroll component's scroll position. This local state is + * passed to the global scroll state update function which stomps the global shared + * values with the local ones as the context provider's value. This will only happen + * if the scene is focused (react-navigation's useIsFocused). In addition to scene + * focus requirement, the caller of this hook has the option to control enabling + * the hook by the optional `isEnabled` boolean parameter. + */ +export const useSceneScrollHandler = (isEnabled: boolean = true): SceneScrollHandler => { + const { updateScrollState } = useSceneScrollContext() + + // Local scroll state const dragStartX = useSharedValue(0) const dragStartY = useSharedValue(0) + const scrollX = useSharedValue(0) + const scrollY = useSharedValue(0) + const scrollBeginEvent = useSharedValue(null) + const scrollEndEvent = useSharedValue(null) + const scrollMomentumBeginEvent = useSharedValue(null) + const scrollMomentumEndEvent = useSharedValue(null) + + const isFocused = useIsFocused() - const handler = useAnimatedScrollHandler( - { - onScroll: (nativeEvent: NativeScrollEvent) => { - 'worklet' - sceneScrollContext.scrollX.value = nativeEvent.contentOffset.y - sceneScrollContext.scrollXDelta.value = nativeEvent.contentOffset.x - dragStartX.value - sceneScrollContext.scrollY.value = nativeEvent.contentOffset.y - sceneScrollContext.scrollYDelta.value = nativeEvent.contentOffset.y - dragStartY.value - }, - onBeginDrag: (nativeEvent: NativeScrollEvent) => { - 'worklet' - dragStartX.value = nativeEvent.contentOffset.x - dragStartY.value = nativeEvent.contentOffset.y - - sceneScrollContext.scrollBeginEvent.value = nativeEvent - }, - onEndDrag: nativeEvent => { - 'worklet' - sceneScrollContext.scrollEndEvent.value = nativeEvent - }, - onMomentumBegin: nativeEvent => { - sceneScrollContext.scrollMomentumBeginEvent.value = nativeEvent - }, - onMomentumEnd: nativeEvent => { - sceneScrollContext.scrollMomentumEndEvent.value = nativeEvent - } + useEffect(() => { + if (isFocused && isEnabled) { + updateScrollState({ + dragStartX, + dragStartY, + scrollX, + scrollY, + scrollBeginEvent, + scrollEndEvent, + scrollMomentumBeginEvent, + scrollMomentumEndEvent + }) + } + }, [ + dragStartX, + dragStartY, + isEnabled, + isFocused, + scrollBeginEvent, + scrollEndEvent, + scrollMomentumBeginEvent, + scrollMomentumEndEvent, + scrollX, + scrollY, + updateScrollState + ]) + + const handler = useAnimatedScrollHandler({ + onScroll: (nativeEvent: NativeScrollEvent) => { + 'worklet' + scrollX.value = nativeEvent.contentOffset.x + scrollY.value = nativeEvent.contentOffset.y + }, + onBeginDrag: (nativeEvent: NativeScrollEvent) => { + 'worklet' + dragStartX.value = nativeEvent.contentOffset.x + dragStartY.value = nativeEvent.contentOffset.y + + scrollBeginEvent.value = nativeEvent + }, + onEndDrag: nativeEvent => { + 'worklet' + scrollEndEvent.value = nativeEvent + }, + onMomentumBegin: nativeEvent => { + scrollMomentumBeginEvent.value = nativeEvent }, - [] - ) + onMomentumEnd: nativeEvent => { + scrollMomentumEndEvent.value = nativeEvent + } + }) return handler }