mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
AnimatedHeader component
This commit is contained in:
@@ -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<number>
|
||||
/**
|
||||
* 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 (
|
||||
<Animated.View style={[animatedHeaderStyle, $headerContainer, { backgroundColor: headerBg }]}>
|
||||
<Animated.Text style={[animatedTitleStyle, $headerTitle, { color: headerTitleColor }]}>
|
||||
{titleContent}
|
||||
</Animated.Text>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
const $headerContainer: ViewStyle = {
|
||||
alignItems: 'center',
|
||||
}
|
||||
|
||||
const $headerTitle: TextStyle = {
|
||||
fontSize: spacing.extraLarge,
|
||||
fontFamily: typography.primary?.medium,
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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<undefined>
|
||||
|
||||
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: () => (
|
||||
<Header
|
||||
titleTx="privacyScreen_title"
|
||||
leftIcon="faArrowLeft"
|
||||
onLeftPress={() => 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 (
|
||||
<Screen style={$screen} preset='fixed'>
|
||||
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
|
||||
<Text preset="heading" tx="privacyScreen_title" style={{color: headerTitle}} />
|
||||
</View>
|
||||
<ScrollView style={$contentContainer}>
|
||||
<AnimatedHeader
|
||||
titleTx="privacyScreen_title"
|
||||
scrollY={scrollY}
|
||||
scrollDistance={HEADER_SCROLL_DISTANCE}
|
||||
/>
|
||||
<Animated.ScrollView
|
||||
style={$contentContainer}
|
||||
onScroll={scrollHandler}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{/*<Card
|
||||
style={[$card, {marginTop: spacing.medium}]}
|
||||
ContentComponent={
|
||||
@@ -370,7 +396,7 @@ export const PrivacyScreen = observer(function PrivacyScreen({ route }: Props) {
|
||||
}
|
||||
/>
|
||||
{isLoading && <Loading statusMessage={statusMessage} />}
|
||||
</ScrollView>
|
||||
</Animated.ScrollView>
|
||||
{/*<BottomModal
|
||||
isVisible={isTorModalVisible ? true : false}
|
||||
ContentComponent={
|
||||
@@ -398,12 +424,6 @@ export const PrivacyScreen = observer(function PrivacyScreen({ route }: Props) {
|
||||
|
||||
const $screen: ViewStyle = {}
|
||||
|
||||
const $headerContainer: TextStyle = {
|
||||
alignItems: 'center',
|
||||
paddingBottom: spacing.medium,
|
||||
height: spacing.screenHeight * 0.15,
|
||||
}
|
||||
|
||||
const $contentContainer: TextStyle = {
|
||||
flex: 1,
|
||||
marginTop: -spacing.extraLarge * 2,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import React, {useState, useEffect, useRef} from 'react'
|
||||
import {Platform, ScrollView, TextInput, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import React, {useState, useEffect, useRef, useLayoutEffect} from 'react'
|
||||
import {Platform, TextInput, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedScrollHandler,
|
||||
} from 'react-native-reanimated'
|
||||
import notifee, { AndroidImportance } from '@notifee/react-native'
|
||||
import {spacing, useThemeColor, colors} from '../theme'
|
||||
import {
|
||||
Button,
|
||||
Button,
|
||||
Card,
|
||||
Screen,
|
||||
ListItem,
|
||||
@@ -13,8 +17,9 @@ import {
|
||||
InfoModal,
|
||||
Loading,
|
||||
BottomModal,
|
||||
Header,
|
||||
AnimatedHeader,
|
||||
} from '../components'
|
||||
import {useHeader} from '../utils/useHeader'
|
||||
import {log} from '../services/logService'
|
||||
import AppError, { Err } from '../utils/AppError'
|
||||
import { useStores } from '../models'
|
||||
@@ -33,11 +38,24 @@ type Props = StaticScreenProps<undefined>
|
||||
|
||||
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: () => (
|
||||
<Header
|
||||
titleTx="recoveryOptionsTitle"
|
||||
leftIcon="faArrowLeft"
|
||||
onLeftPress={() => navigation.goBack()}
|
||||
scrollY={scrollY}
|
||||
scrollDistance={HEADER_SCROLL_DISTANCE}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}, [])
|
||||
|
||||
const {mintsStore, proofsStore} = useStores()
|
||||
|
||||
const unitRef = useRef<MintUnit>('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 (
|
||||
<Screen preset="fixed" contentContainerStyle={$screen}>
|
||||
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
|
||||
<Text
|
||||
preset="heading"
|
||||
tx="recoveryOptionsTitle"
|
||||
style={{color: 'white'}}
|
||||
/>
|
||||
</View>
|
||||
<ScrollView style={$contentContainer}>
|
||||
<AnimatedHeader
|
||||
titleTx="recoveryOptionsTitle"
|
||||
scrollY={scrollY}
|
||||
scrollDistance={HEADER_SCROLL_DISTANCE}
|
||||
/>
|
||||
<Animated.ScrollView
|
||||
style={$contentContainer}
|
||||
onScroll={scrollHandler}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
<Card
|
||||
style={$card}
|
||||
HeadingComponent={
|
||||
@@ -413,10 +437,10 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen(_:
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{isLoading && <Loading />}
|
||||
{isLoading && <Loading />}
|
||||
{error && <ErrorModal error={error} />}
|
||||
{info && <InfoModal message={info} />}
|
||||
</ScrollView>
|
||||
{info && <InfoModal message={info} />}
|
||||
</Animated.ScrollView>
|
||||
<BottomModal
|
||||
isVisible={isNotificationModalVisible ? true : false}
|
||||
ContentComponent={
|
||||
@@ -575,12 +599,6 @@ const $screen: ViewStyle = {
|
||||
// flex: 1,
|
||||
}
|
||||
|
||||
const $headerContainer: TextStyle = {
|
||||
alignItems: 'center',
|
||||
paddingBottom: spacing.medium,
|
||||
height: spacing.screenHeight * 0.15,
|
||||
}
|
||||
|
||||
const $contentContainer: TextStyle = {
|
||||
//flex: 1,
|
||||
marginTop: -spacing.extraLarge * 2,
|
||||
|
||||
@@ -3,10 +3,7 @@ import React, {FC, useEffect, useLayoutEffect, useRef, useState} from 'react'
|
||||
import {AppState, Platform, TextStyle, View, ViewStyle, useColorScheme} from 'react-native'
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
useAnimatedScrollHandler,
|
||||
interpolate,
|
||||
Extrapolation,
|
||||
} from 'react-native-reanimated'
|
||||
import notifee, { AuthorizationStatus } from '@notifee/react-native'
|
||||
import messaging from '@react-native-firebase/messaging'
|
||||
@@ -15,8 +12,8 @@ import {
|
||||
HOT_UPDATER_API_KEY,
|
||||
HOT_UPDATER_URL,
|
||||
} from '@env'
|
||||
import {ThemeCode, Themes, colors, spacing, typography, useThemeColor} from '../theme'
|
||||
import {ListItem, Screen, Text, Card, NwcIcon, Button, BottomModal, InfoModal, Icon, Header} from '../components'
|
||||
import {ThemeCode, Themes, colors, spacing, useThemeColor} from '../theme'
|
||||
import {ListItem, Screen, Text, Card, NwcIcon, Button, BottomModal, InfoModal, Icon, Header, AnimatedHeader} from '../components'
|
||||
import {useStores} from '../models'
|
||||
import {translate} from '../i18n'
|
||||
import { log } from '../services'
|
||||
@@ -304,65 +301,22 @@ export const SettingsScreen = observer(function SettingsScreen({ route }: Props)
|
||||
|
||||
|
||||
const $itemRight = {color: useThemeColor('textDim')}
|
||||
const headerBg = useThemeColor('header')
|
||||
const headerTitle = useThemeColor('headerTitle')
|
||||
const colorScheme = useColorScheme()
|
||||
const defaultThemeColor = colorScheme === 'dark' ? Themes[ThemeCode.DARK]?.color : Themes[ThemeCode.LIGHT]?.color
|
||||
|
||||
// Collapsible header animation
|
||||
const HEADER_MAX_HEIGHT = spacing.screenHeight * 0.15
|
||||
const HEADER_MIN_HEIGHT = spacing.screenHeight * 0.08
|
||||
|
||||
const scrollHandler = useAnimatedScrollHandler({
|
||||
onScroll: (event) => {
|
||||
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 (
|
||||
<Screen contentContainerStyle={$screen} preset='fixed'>
|
||||
<Animated.View style={[animatedHeaderStyle, $headerContainer, {backgroundColor: headerBg}]}>
|
||||
<Animated.Text
|
||||
style={[animatedTitleStyle, $headerTitle, {color: headerTitle}]}
|
||||
>
|
||||
{translate('settingsScreen_title')}
|
||||
</Animated.Text>
|
||||
</Animated.View>
|
||||
<AnimatedHeader
|
||||
titleTx="settingsScreen_title"
|
||||
scrollY={scrollY}
|
||||
scrollDistance={HEADER_SCROLL_DISTANCE}
|
||||
/>
|
||||
<Animated.ScrollView
|
||||
style={$contentContainer}
|
||||
onScroll={scrollHandler}
|
||||
@@ -637,20 +591,6 @@ const $screen: ViewStyle = {
|
||||
|
||||
}
|
||||
|
||||
const $headerContainer: ViewStyle = {
|
||||
alignItems: 'center',
|
||||
//justifyContent: 'flex-end',
|
||||
//paddingBottom: spacing.extraLarge * 2 + spacing.medium,
|
||||
//overflow: 'visible',
|
||||
}
|
||||
|
||||
const $headerTitle: TextStyle = {
|
||||
fontSize: spacing.extraLarge,
|
||||
//lineHeight: 44,
|
||||
fontFamily: typography.primary?.medium,
|
||||
}
|
||||
|
||||
|
||||
const $contentContainer: TextStyle = {
|
||||
marginTop: -spacing.extraLarge * 2,
|
||||
padding: spacing.extraSmall,
|
||||
|
||||
Reference in New Issue
Block a user