diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap index bef4933e6dc..1e1cfc74e65 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap @@ -209,6 +209,7 @@ exports[`CreateWalletAccountSelect renders 1`] = ` , diff --git a/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap index 2f1101ac848..6836e3ec0c5 100644 --- a/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap @@ -209,6 +209,7 @@ exports[`CurrencyNotificationComponent should render with loading props 1`] = ` , diff --git a/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap index dece7fe5619..fccf9de5ceb 100644 --- a/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap @@ -209,6 +209,7 @@ exports[`CurrencySettings should render 1`] = ` , diff --git a/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap index 76bf9826eae..72b93cb547d 100644 --- a/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap @@ -209,6 +209,7 @@ exports[`MyComponent should render Locked SettingsOverview 1`] = ` , @@ -3644,6 +3643,7 @@ exports[`MyComponent should render UnLocked SettingsOverview 1`] = ` , diff --git a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap index 5a139b61241..b98e269b99b 100644 --- a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap @@ -164,6 +164,7 @@ exports[`TransactionDetailsScene should render 1`] = ` , @@ -1882,6 +1881,7 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi , diff --git a/src/components/common/SceneWrapper.tsx b/src/components/common/SceneWrapper.tsx index 3e667c34327..a0309044176 100644 --- a/src/components/common/SceneWrapper.tsx +++ b/src/components/common/SceneWrapper.tsx @@ -2,10 +2,12 @@ import { getDefaultHeaderHeight } from '@react-navigation/elements' import { useNavigation } from '@react-navigation/native' import * as React from 'react' import { useMemo } from 'react' -import { Animated, ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native' +import { Animated, StyleSheet, useWindowDimensions, View } from 'react-native' +import Reanimated from 'react-native-reanimated' import { EdgeInsets, useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context' import { useSceneDrawerState } from '../../state/SceneDrawerState' +import { useSceneScrollHandler } from '../../state/SceneScrollState' import { useSelector } from '../../types/reactRedux' import { NavigationBase } from '../../types/routerTypes' import { OverrideDots } from '../../types/Theme' @@ -129,6 +131,9 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { const notificationHeight = theme.rem(4) const headerBarHeight = getDefaultHeaderHeight(frame, false, 0) + // If the scene has scroll, this will be required for tabs and/or header animation + 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 const hasKeyboardAnimation = keyboardAnimation != null @@ -172,18 +177,19 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { backgroundGradientStart={backgroundGradientStart} backgroundGradientEnd={backgroundGradientEnd} /> - {}} > {isFuncChildren ? children(info) : children} {hasNotifications ? : null} {renderDrawer == null ? null : {renderDrawer}} - + ) } @@ -218,5 +224,5 @@ const styles = StyleSheet.create({ }) const MaybeAnimatedView = maybeComponent(Animated.View) -const MaybeScrollView = maybeComponent(ScrollView) +const MaybeAnimatedScrollView = maybeComponent(Reanimated.ScrollView) const MaybeView = maybeComponent(View) 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 788bf6912b9..a49305944f8 100644 --- a/src/state/SceneScrollState.tsx +++ b/src/state/SceneScrollState.tsx @@ -1,63 +1,154 @@ -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() + + 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' - sceneScrollContext.scrollX.value = nativeEvent.contentOffset.y - sceneScrollContext.scrollXDelta.value = nativeEvent.contentOffset.y - dragStartY.value - sceneScrollContext.scrollY.value = nativeEvent.contentOffset.y - sceneScrollContext.scrollYDelta.value = nativeEvent.contentOffset.y - dragStartY.value + scrollX.value = nativeEvent.contentOffset.x + scrollY.value = nativeEvent.contentOffset.y }, onBeginDrag: (nativeEvent: NativeScrollEvent) => { 'worklet' + dragStartX.value = nativeEvent.contentOffset.x dragStartY.value = nativeEvent.contentOffset.y - sceneScrollContext.scrollBeginEvent.value = nativeEvent + scrollBeginEvent.value = nativeEvent }, onEndDrag: nativeEvent => { 'worklet' - sceneScrollContext.scrollEndEvent.value = nativeEvent + scrollEndEvent.value = nativeEvent }, onMomentumBegin: nativeEvent => { - sceneScrollContext.scrollMomentumBeginEvent.value = nativeEvent + scrollMomentumBeginEvent.value = nativeEvent }, onMomentumEnd: nativeEvent => { - sceneScrollContext.scrollMomentumEndEvent.value = nativeEvent + scrollMomentumEndEvent.value = nativeEvent } })