feat(mobile): optimize tab bar interaction and scroll behavior

- Extract TabItem component with improved press animations
- Simplify native scroll to top implementation
- Add cross-platform scroll to top method
- Enhance tab bar interaction with animated scaling effect

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-02-25 22:33:21 +08:00
parent 265f23fd76
commit 70e115c73f
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
4 changed files with 81 additions and 19 deletions

View File

@ -2,7 +2,7 @@ import type { BottomTabBarProps } from "@react-navigation/bottom-tabs"
import { getLabel } from "@react-navigation/elements"
import { CommonActions, NavigationContext, NavigationRouteContext } from "@react-navigation/native"
import type { FC } from "react"
import { useContext, useEffect } from "react"
import { memo, useContext, useEffect } from "react"
import type { StyleProp, TextStyle } from "react-native"
import { Platform, Pressable, StyleSheet, View } from "react-native"
import Animated, {
@ -127,19 +127,18 @@ export const Tabbar: FC<BottomTabBarProps> = (props) => {
/>
)
}
const scene = { route, focused }
return (
<NavigationContext.Provider key={route.key} value={descriptors[route.key]?.navigation}>
<NavigationRouteContext.Provider value={route}>
<Pressable
onPress={onPress}
className="flex-1 flex-col items-center justify-center"
>
{renderIcon(scene)}
{renderLabel(scene)}
</Pressable>
</NavigationRouteContext.Provider>
</NavigationContext.Provider>
<TabItem
key={route.key}
route={route}
focused={focused}
descriptors={descriptors}
onPress={onPress}
originalRenderIcon={renderIcon}
originalRenderLabel={renderLabel}
accessibilityLabel={accessibilityLabel}
/>
)
})}
</Grid>
@ -260,3 +259,57 @@ const TabBarBackground = () => {
const borderColor = useColor("opaqueSeparator")
return <AnimatedThemedBlurView style={[styles.blurEffect, animatedStyle, { borderColor }]} />
}
const TabItem = memo(
// eslint-disable-next-line @eslint-react/no-unstable-context-value
({
route,
focused,
descriptors,
onPress,
originalRenderIcon,
originalRenderLabel,
accessibilityLabel,
}: {
route: any
focused: boolean
descriptors: any
onPress: () => void
originalRenderIcon: (scene: { route: any; focused: boolean }) => React.ReactNode
originalRenderLabel: (scene: { route: any; focused: boolean }) => React.ReactNode
accessibilityLabel?: string
}) => {
const pressed = useSharedValue(0)
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: 1 - 0.15 * pressed.value }],
}
})
const scene = { route, focused }
return (
<NavigationContext.Provider value={descriptors[route.key]?.navigation}>
<NavigationRouteContext.Provider value={route}>
<Pressable
onPress={onPress}
onPressIn={() => {
pressed.value = withSpring(1, { damping: 15, stiffness: 150 })
}}
onPressOut={() => {
pressed.value = withSpring(0, { damping: 15, stiffness: 150 })
}}
className="flex-1 flex-col items-center justify-center"
accessibilityLabel={accessibilityLabel}
accessibilityRole="button"
accessibilityState={{ selected: focused }}
>
<Animated.View style={animatedStyle}>{originalRenderIcon(scene)}</Animated.View>
{originalRenderLabel(scene)}
</Pressable>
</NavigationRouteContext.Provider>
</NavigationContext.Provider>
)
},
)

View File

@ -1,11 +1,9 @@
import type { FlashList } from "@shopify/flash-list"
import { requireNativeModule } from "expo"
import type { RefObject } from "react"
import { useCallback, useContext, useEffect, useRef } from "react"
import type { FlatList, ScrollView, View } from "react-native"
import type { FlatList, ScrollView } from "react-native"
import { findNodeHandle, Platform } from "react-native"
import PlaterScreen from "@/src/screens/player"
import { performNativeScrollToTop } from "@/src/lib/native"
import {
AttachNavigationScrollViewContext,
@ -28,8 +26,10 @@ export const useNavigationScrollToTop = (
if (Platform.OS === "ios") {
const reactTag = findNodeHandle($scroller)
requireNativeModule("Helper").scrollToTop(reactTag)
return
if (reactTag) {
performNativeScrollToTop(reactTag)
return
}
}
if ("scrollTo" in $scroller) {

View File

@ -3,6 +3,7 @@ import { requireNativeModule } from "expo"
interface NativeModule {
openLink: (url: string) => void
previewImage: (images: string[]) => void
scrollToTop: (reactTag: number) => void
}
const nativeModule = requireNativeModule("Helper") as NativeModule
export const openLink = (url: string) => {
@ -11,3 +12,7 @@ export const openLink = (url: string) => {
export const quickLookImage = (images: string[]) => {
nativeModule.previewImage(images)
}
export const performNativeScrollToTop = (reactTag: number) => {
nativeModule.scrollToTop(reactTag)
}

View File

@ -5,3 +5,7 @@ export const openLink = (url: string) => {
}
export const quickLookImage = (_images: string[]) => {}
export const performNativeScrollToTop = (_reactTag: number) => {
throw new Error("performNativeScrollToTop is not supported on this platform")
}