Skip to content

Commit e52e03f

Browse files
committed
feat(mobile): improve header layout and animation
- Add smooth centering animation for navigation header title - Implement blur effect in search and discover headers - Standardize header action group width - Remove unnecessary refresh control in search tabs
1 parent 09ed50a commit e52e03f

File tree

4 files changed

+52
-74
lines changed

4 files changed

+52
-74
lines changed

apps/mobile/src/components/layouts/header/NavigationHeader.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Animated, {
99
useAnimatedRef,
1010
useAnimatedStyle,
1111
useSharedValue,
12+
withSpring,
1213
withTiming,
1314
} from "react-native-reanimated"
1415
import type { DefaultStyle } from "react-native-reanimated/lib/typescript/hook/commonTypes"
@@ -212,6 +213,7 @@ export const NavigationHeader = ({
212213
const [titleWidth, setTitleWidth] = useState(0)
213214

214215
const titleShouldCenterTransformX = useMemo(() => {
216+
if (!titleWidth) return 0
215217
const halfTitleWidth = titleWidth / 2
216218
const currentTitleCenterX =
217219
titlebarPaddingHorizontal + headerLeftWidth + titleMarginHorizontal + halfTitleWidth
@@ -220,6 +222,11 @@ export const NavigationHeader = ({
220222
const transformX = centerX - currentTitleCenterX
221223
return transformX
222224
}, [titleBarWidth, titleWidth, headerLeftWidth])
225+
const titleTransformX = useSharedValue(0)
226+
227+
useEffect(() => {
228+
titleTransformX.value = withSpring(titleShouldCenterTransformX)
229+
}, [titleShouldCenterTransformX, titleTransformX])
223230

224231
return (
225232
<Animated.View
@@ -261,19 +268,19 @@ export const NavigationHeader = ({
261268
<HeaderLeft canGoBack={canBack} />
262269
</View>
263270
{/* Center */}
264-
<View
271+
<Animated.View
265272
onLayout={useCallback((e: LayoutChangeEvent) => {
266273
setTitleWidth(e.nativeEvent.layout.width)
267274
}, [])}
268275
className="flex-1 items-center justify-center"
269276
pointerEvents={"box-none"}
270277
style={{
271278
marginHorizontal: titleMarginHorizontal,
272-
transform: [{ translateX: titleShouldCenterTransformX }],
279+
transform: [{ translateX: titleTransformX }],
273280
}}
274281
>
275282
{headerTitle}
276-
</View>
283+
</Animated.View>
277284
{/* Right */}
278285
<View className="min-w-6 flex-row items-center justify-end" pointerEvents={"box-none"}>
279286
<RightButton canGoBack={canBack} />

apps/mobile/src/modules/discover/search-tabs/__base.tsx

Lines changed: 3 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
import { cn } from "@follow/utils/src/utils"
2-
import { forwardRef, useCallback, useState } from "react"
3-
import type { NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps } from "react-native"
4-
import {
5-
RefreshControl,
6-
ScrollView,
7-
useAnimatedValue,
8-
useWindowDimensions,
9-
View,
10-
} from "react-native"
2+
import { forwardRef } from "react"
3+
import type { ScrollViewProps } from "react-native"
4+
import { ScrollView, useWindowDimensions, View } from "react-native"
115
import type { FlatListPropsWithLayout } from "react-native-reanimated"
126
import ReAnimated, { LinearTransition } from "react-native-reanimated"
137
import { useSafeAreaInsets } from "react-native-safe-area-context"
148

15-
import { CustomRefreshControl } from "@/src/components/common/RefreshControl"
16-
179
import { useSearchBarHeight } from "../ctx"
1810

1911
export const BaseSearchPageScrollView = forwardRef<ScrollView, ScrollViewProps>(
@@ -66,38 +58,6 @@ export function BaseSearchPageFlatList<T>({
6658
const offsetTop = searchBarHeight - insets.top
6759
const windowWidth = useWindowDimensions().width
6860

69-
const [currentRefreshing, setCurrentRefreshing] = useState(refreshing)
70-
const nextRefreshing = currentRefreshing || refreshing
71-
72-
const [pullProgress, setPullProgress] = useState(0)
73-
74-
const scrollY = useAnimatedValue(0)
75-
76-
const THRESHOLD = 180
77-
78-
const handleRefresh = async () => {
79-
setCurrentRefreshing(true)
80-
try {
81-
await onRefresh()
82-
} finally {
83-
setCurrentRefreshing(false)
84-
}
85-
}
86-
87-
const handleScroll = useCallback(
88-
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
89-
const offsetY = event.nativeEvent.contentOffset.y
90-
if (offsetY < 0) {
91-
const progress = Math.abs(offsetY) / THRESHOLD
92-
setPullProgress(progress)
93-
} else {
94-
setPullProgress(0)
95-
}
96-
scrollY.setValue(offsetY)
97-
},
98-
[scrollY],
99-
)
100-
10161
return (
10262
<>
10363
<ReAnimated.FlatList
@@ -108,19 +68,6 @@ export function BaseSearchPageFlatList<T>({
10868
scrollIndicatorInsets={{ bottom: insets.bottom, top: offsetTop }}
10969
automaticallyAdjustContentInsets
11070
contentInsetAdjustmentBehavior="always"
111-
refreshControl={
112-
<View style={{ transform: [{ translateY: offsetTop }] }}>
113-
<CustomRefreshControl refreshing={nextRefreshing} pullProgress={pullProgress} />
114-
<RefreshControl
115-
tintColor="transparent"
116-
colors={["transparent"]}
117-
className="bg-transparent"
118-
refreshing={nextRefreshing}
119-
onRefresh={handleRefresh}
120-
/>
121-
</View>
122-
}
123-
onScroll={handleScroll}
12471
scrollEventThrottle={16}
12572
{...props}
12673
/>

apps/mobile/src/modules/discover/search.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Animated, {
1414
} from "react-native-reanimated"
1515
import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-context"
1616

17+
import { BlurEffect } from "@/src/components/common/BlurEffect"
1718
import { TabBar } from "@/src/components/ui/tabview/TabBar"
1819
import { Search2CuteReIcon } from "@/src/icons/search_2_cute_re"
1920
import { accentColor, useColor } from "@/src/theme/colors"
@@ -38,6 +39,7 @@ export const SearchHeader: FC<{
3839
className="relative"
3940
onLayout={onLayout}
4041
>
42+
<BlurEffect />
4143
<View style={styles.header}>
4244
<ComposeSearchBar />
4345
</View>
@@ -56,16 +58,16 @@ const DiscoverHeaderImpl = () => {
5658
const { animatedX, currentTabAtom, headerHeightAtom } = useContext(DiscoverContext)
5759
const setCurrentTab = useSetAtom(currentTabAtom)
5860
const setHeaderHeight = useSetAtom(headerHeightAtom)
59-
const bgColor = useColor("systemBackground")
6061

6162
return (
6263
<View
63-
style={{ minHeight: headerHeight, paddingTop: insets.top, backgroundColor: bgColor }}
64+
style={{ minHeight: headerHeight, paddingTop: insets.top }}
6465
className="relative"
6566
onLayout={(e) => {
6667
setHeaderHeight(e.nativeEvent.layout.height)
6768
}}
6869
>
70+
<BlurEffect />
6971
<View style={[styles.header, styles.discoverHeader]}>
7072
<PlaceholerSearchBar />
7173

apps/mobile/src/modules/screen/TimelineSelectorProvider.tsx

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { getFeed } from "@/src/store/feed/getter"
1919

2020
import { useEntryListContext, useSelectedFeedTitle } from "./atoms"
2121

22+
const HEADER_ACTIONS_GROUP_WIDTH = 60
2223
export function TimelineSelectorProvider({ children }: { children: React.ReactNode }) {
2324
const scrollY = useAnimatedValue(0)
2425
const viewTitle = useSelectedFeedTitle()
@@ -33,23 +34,44 @@ export function TimelineSelectorProvider({ children }: { children: React.ReactNo
3334
headerShown
3435
title={viewTitle}
3536
headerLeft={useMemo(
36-
() => (isTimeline || isSubscriptions ? () => <HomeLeftAction /> : undefined),
37+
() =>
38+
isTimeline || isSubscriptions
39+
? () => (
40+
<View style={{ width: HEADER_ACTIONS_GROUP_WIDTH }}>
41+
<HomeLeftAction />
42+
</View>
43+
)
44+
: undefined,
3745
[isTimeline, isSubscriptions],
3846
)}
3947
headerRight={useMemo(() => {
40-
const buttonVariant = isFeed ? "secondary" : "primary"
41-
if (isTimeline)
42-
return () => (
43-
<HomeSharedRightAction>
44-
<UnreadOnlyActionButton variant={buttonVariant} />
45-
</HomeSharedRightAction>
46-
)
47-
if (isSubscriptions) return () => <HomeSharedRightAction />
48-
if (isFeed)
48+
const Component = (() => {
49+
const buttonVariant = isFeed ? "secondary" : "primary"
50+
if (isTimeline)
51+
return () => (
52+
<HomeSharedRightAction>
53+
<UnreadOnlyActionButton variant={buttonVariant} />
54+
</HomeSharedRightAction>
55+
)
56+
if (isSubscriptions) return () => <HomeSharedRightAction />
57+
if (isFeed)
58+
return () => (
59+
<View className="-mr-2 flex-row gap-2">
60+
<UnreadOnlyActionButton variant={buttonVariant} />
61+
<FeedShareAction params={params} />
62+
</View>
63+
)
64+
})()
65+
66+
if (Component)
4967
return () => (
50-
<View className="-mr-2 flex-row gap-2">
51-
<UnreadOnlyActionButton variant={buttonVariant} />
52-
<FeedShareAction params={params} />
68+
<View
69+
style={{
70+
width: HEADER_ACTIONS_GROUP_WIDTH,
71+
}}
72+
className="flex-row items-center justify-end"
73+
>
74+
{Component()}
5375
</View>
5476
)
5577
return

0 commit comments

Comments
 (0)