diff --git a/src/components/AnimatedHeader.tsx b/src/components/AnimatedHeader.tsx new file mode 100644 index 00000000..87fc405a --- /dev/null +++ b/src/components/AnimatedHeader.tsx @@ -0,0 +1,112 @@ +import React from 'react' +import { TextStyle, ViewStyle } from 'react-native' +import Animated, { + SharedValue, + useAnimatedStyle, + interpolate, + Extrapolation, +} from 'react-native-reanimated' +import { spacing, typography, useThemeColor } from '../theme' +import { translate, TxKeyPath } from '../i18n' + +export interface AnimatedHeaderProps { + /** + * Title text to display. + */ + title?: string + /** + * Title text which is looked up via i18n. + */ + titleTx?: TxKeyPath + /** + * Shared value for scroll position tracking. + */ + scrollY: SharedValue + /** + * The scroll distance over which the animation occurs. + * Defaults to spacing.screenHeight * 0.07 + */ + scrollDistance?: number + /** + * Maximum header height when not scrolled. + * Defaults to spacing.screenHeight * 0.15 + */ + maxHeight?: number + /** + * Minimum header height when fully scrolled. + * Defaults to spacing.screenHeight * 0.08 + */ + minHeight?: number +} + +/** + * AnimatedHeader component that provides a collapsible header with animated title. + * Use with Animated.ScrollView and useAnimatedScrollHandler to track scroll position. + */ +export function AnimatedHeader(props: AnimatedHeaderProps) { + const { + title, + titleTx, + scrollY, + scrollDistance = spacing.screenHeight * 0.07, + maxHeight = spacing.screenHeight * 0.15, + minHeight = spacing.screenHeight * 0.08, + } = props + + const headerBg = useThemeColor('header') + const headerTitleColor = useThemeColor('headerTitle') + + const animatedHeaderStyle = useAnimatedStyle(() => { + const height = interpolate( + scrollY.value, + [0, scrollDistance], + [maxHeight, minHeight], + Extrapolation.CLAMP + ) + return { height } + }) + + const animatedTitleStyle = useAnimatedStyle(() => { + const scale = interpolate( + scrollY.value, + [0, scrollDistance], + [1, 0.75], + Extrapolation.CLAMP + ) + const translateY = interpolate( + scrollY.value, + [0, scrollDistance], + [0, -spacing.extraLarge * 1.5], + Extrapolation.CLAMP + ) + const opacity = interpolate( + scrollY.value, + [0, scrollDistance * 0.8], + [1, 0], + Extrapolation.CLAMP + ) + return { + transform: [{ scale }, { translateY }], + opacity, + } + }) + + const titleContent = titleTx ? translate(titleTx) : title + + return ( + + + {titleContent} + + + ) +} + +const $headerContainer: ViewStyle = { + alignItems: 'center', +} + +const $headerTitle: TextStyle = { + fontSize: spacing.extraLarge, + fontFamily: typography.primary?.medium, +} diff --git a/src/components/index.ts b/src/components/index.ts index abf66aea..2c916d60 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,6 +3,7 @@ export * from "./AutoImage" export * from "./Button" export * from "./Card" export * from "./Header" +export * from "./AnimatedHeader" export * from "./Icon" export * from "./ListItem" export * from "./Screen" diff --git a/src/screens/PrivacyScreen.tsx b/src/screens/PrivacyScreen.tsx index a95a4c10..6b48232f 100644 --- a/src/screens/PrivacyScreen.tsx +++ b/src/screens/PrivacyScreen.tsx @@ -1,6 +1,10 @@ import {observer} from 'mobx-react-lite' -import React, {FC, useEffect, useState} from 'react' -import {Switch, TextStyle, View, ViewStyle, ScrollView} from 'react-native' +import React, {FC, useEffect, useState, useLayoutEffect} from 'react' +import {Switch, TextStyle, View, ViewStyle} from 'react-native' +import Animated, { + useSharedValue, + useAnimatedScrollHandler, +} from 'react-native-reanimated' import {colors, spacing, useThemeColor} from '../theme' import { ListItem, @@ -12,8 +16,9 @@ import { InfoModal, BottomModal, Button, + Header, + AnimatedHeader, } from '../components' -import {useHeader} from '../utils/useHeader' import {useStores} from '../models' import AppError from '../utils/AppError' import { KeyChain, MinibitsClient, log } from '../services' @@ -25,12 +30,23 @@ type Props = StaticScreenProps export const PrivacyScreen = observer(function PrivacyScreen({ route }: Props) { const navigation = useNavigation() - useHeader({ - leftIcon: 'faArrowLeft', - onLeftPress: () => { - navigation.goBack() - } - }) + const scrollY = useSharedValue(0) + const HEADER_SCROLL_DISTANCE = spacing.screenHeight * 0.07 + + useLayoutEffect(() => { + navigation.setOptions({ + headerShown: true, + header: () => ( +
navigation.goBack()} + scrollY={scrollY} + scrollDistance={HEADER_SCROLL_DISTANCE} + /> + ), + }) + }, []) const {userSettingsStore, walletProfileStore, walletStore, authStore} = useStores() const [info, setInfo] = useState('') @@ -180,16 +196,26 @@ export const PrivacyScreen = observer(function PrivacyScreen({ route }: Props) { setError(e) } - const headerBg = useThemeColor('header') const iconColor = useThemeColor('textDim') - const headerTitle = useThemeColor('headerTitle') + + const scrollHandler = useAnimatedScrollHandler({ + onScroll: (event) => { + scrollY.value = event.contentOffset.y + }, + }) return ( - - - - + + {/* {isLoading && } - + {/* export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen(_: Props) { const navigation = useNavigation() - useHeader({ - leftIcon: 'faArrowLeft', - onLeftPress: () => navigation.goBack(), - }) - + const scrollY = useSharedValue(0) + const HEADER_SCROLL_DISTANCE = spacing.screenHeight * 0.07 + + useLayoutEffect(() => { + navigation.setOptions({ + headerShown: true, + header: () => ( +
navigation.goBack()} + scrollY={scrollY} + scrollDistance={HEADER_SCROLL_DISTANCE} + /> + ), + }) + }, []) + const {mintsStore, proofsStore} = useStores() const unitRef = useRef('sat') @@ -297,22 +315,28 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen(_: setMintBalanceToRecoverFrom(mintBalancesRef.current[0]) } - const headerBg = useThemeColor('header') const inputBg = useThemeColor('background') const mintsModalBg = useThemeColor('background') const inputText = useThemeColor('text') - + + const scrollHandler = useAnimatedScrollHandler({ + onScroll: (event) => { + scrollY.value = event.contentOffset.y + }, + }) return ( - - - - + + } /> - {isLoading && } + {isLoading && } {error && } - {info && } - + {info && } + { scrollY.value = event.contentOffset.y }, }) - const animatedHeaderStyle = useAnimatedStyle(() => { - const height = interpolate( - scrollY.value, - [0, HEADER_SCROLL_DISTANCE], - [HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT], - Extrapolation.CLAMP - ) - return { height } - }) - - const animatedTitleStyle = useAnimatedStyle(() => { - const scale = interpolate( - scrollY.value, - [0, HEADER_SCROLL_DISTANCE], - [1, 0.75], - Extrapolation.CLAMP - ) - const translateY = interpolate( - scrollY.value, - [0, HEADER_SCROLL_DISTANCE], - [0, -spacing.extraLarge * 1.5], - Extrapolation.CLAMP - ) - const opacity = interpolate( - scrollY.value, - [0, HEADER_SCROLL_DISTANCE * 0.8], - [1, 0], - Extrapolation.CLAMP - ) - return { - transform: [{ scale }, { translateY }], - opacity, - } - }) - return ( - - - {translate('settingsScreen_title')} - - +