From 70e115c73fcdab9d4fb86d5492249c4eb3595440 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 25 Feb 2025 22:33:21 +0800 Subject: [PATCH] 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 --- .../src/components/ui/tabbar/Tabbar.tsx | 79 ++++++++++++++++--- apps/mobile/src/components/ui/tabbar/hooks.ts | 12 +-- apps/mobile/src/lib/native/index.ios.ts | 5 ++ apps/mobile/src/lib/native/index.ts | 4 + 4 files changed, 81 insertions(+), 19 deletions(-) diff --git a/apps/mobile/src/components/ui/tabbar/Tabbar.tsx b/apps/mobile/src/components/ui/tabbar/Tabbar.tsx index 3883dc1ec..ad76994df 100644 --- a/apps/mobile/src/components/ui/tabbar/Tabbar.tsx +++ b/apps/mobile/src/components/ui/tabbar/Tabbar.tsx @@ -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 = (props) => { /> ) } - const scene = { route, focused } + return ( - - - - {renderIcon(scene)} - {renderLabel(scene)} - - - + ) })} @@ -260,3 +259,57 @@ const TabBarBackground = () => { const borderColor = useColor("opaqueSeparator") return } + +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 ( + + + { + 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 }} + > + {originalRenderIcon(scene)} + {originalRenderLabel(scene)} + + + + ) + }, +) diff --git a/apps/mobile/src/components/ui/tabbar/hooks.ts b/apps/mobile/src/components/ui/tabbar/hooks.ts index 2ccf8a9ac..30324dd3e 100644 --- a/apps/mobile/src/components/ui/tabbar/hooks.ts +++ b/apps/mobile/src/components/ui/tabbar/hooks.ts @@ -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) { diff --git a/apps/mobile/src/lib/native/index.ios.ts b/apps/mobile/src/lib/native/index.ios.ts index 62527a570..cd2d656cd 100644 --- a/apps/mobile/src/lib/native/index.ios.ts +++ b/apps/mobile/src/lib/native/index.ios.ts @@ -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) +} diff --git a/apps/mobile/src/lib/native/index.ts b/apps/mobile/src/lib/native/index.ts index 85abed83b..8164626eb 100644 --- a/apps/mobile/src/lib/native/index.ts +++ b/apps/mobile/src/lib/native/index.ts @@ -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") +}