Skip to content

Commit

Permalink
refactor(rn): discover header to apply blur effect
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <[email protected]>
  • Loading branch information
Innei committed Jan 23, 2025
1 parent e64da25 commit 7d98e27
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 40 deletions.
2 changes: 1 addition & 1 deletion apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({

name: "Follow",
slug: "follow",
version: __DEV__ ? "dev" : PKG.version,
version: process.env.NODE_ENV === "development" ? "dev" : PKG.version,
orientation: "portrait",
icon: iconPath,
scheme: "follow",
Expand Down
40 changes: 31 additions & 9 deletions apps/mobile/src/components/ui/tabview/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,39 @@ export const TabBar = forwardRef<ScrollView, TabBarProps>(
useEffect(() => {
if (tabWidths.length > 0) {
indicatorPosition.value = withSpring(tabPositions[currentTab] || 0, springConfig)
}
}, [currentTab, indicatorPosition, tabPositions, tabWidths.length])

const tabBarScrollX = useRef(0)
useEffect(() => {
// If the current tab is not within the visible range of the scrollview, then scroll the scrollview to the visible area.
if (tabRef.current && tabPositions[currentTab] !== undefined && tabWidths[currentTab]) {
const tabPosition = tabPositions[currentTab]
const tabWidth = tabWidths[currentTab]

if (tabRef.current) {
const x = currentTab > 0 ? tabPositions[currentTab - 1]! + tabWidths[currentTab - 1]! : 0
// Get the current scroll position and visible width of the ScrollView
const scrollView = tabRef.current
const currentScrollX = tabBarScrollX.current

const isCurrentTabVisible =
sharedPagerOffsetX.value < tabPositions[currentTab]! &&
sharedPagerOffsetX.value + tabWidths[currentTab]! > tabPositions[currentTab]!
const visibleWidth = tabBarWidth

if (!isCurrentTabVisible) {
tabRef.current.scrollTo({ x, y: 0, animated: true })
}
// Check if the tab is outside the visible area
const isTabOutsideView =
tabPosition < currentScrollX || // tab is to the left of visible area
tabPosition + tabWidth > currentScrollX + visibleWidth // tab is to the right

if (isTabOutsideView) {
// Add some padding to ensure the tab isn't right at the edge
const padding = 16

scrollView.scrollTo({
x: Math.max(0, tabPosition - padding),
animated: true,
})
}
}
}, [currentTab, indicatorPosition, sharedPagerOffsetX.value, tabPositions, tabWidths])
}, [currentTab, sharedPagerOffsetX.value, tabPositions, tabWidths, tabBarWidth])

const handleTabItemLayout = useCallback((event: LayoutChangeEvent, index: number) => {
const { width, x } = event.nativeEvent.layout
setTabWidths((prev) => {
Expand Down Expand Up @@ -165,6 +184,9 @@ export const TabBar = forwardRef<ScrollView, TabBarProps>(
onLayout={(event) => {
setTabBarWidth(event.nativeEvent.layout.width)
}}
onScroll={(event) => {
tabBarScrollX.current = event.nativeEvent.contentOffset.x
}}
showsHorizontalScrollIndicator={false}
className={cn(
"border-tertiary-system-background relative shrink-0 grow-0",
Expand Down
10 changes: 10 additions & 0 deletions apps/mobile/src/modules/discover/DiscoverContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { PrimitiveAtom } from "jotai"
import { createContext } from "react"
import type { Animated } from "react-native"

export const DiscoverContext = createContext<{
animatedX: Animated.Value
currentTabAtom: PrimitiveAtom<number>

headerHeightAtom: PrimitiveAtom<number>
}>(null!)
81 changes: 63 additions & 18 deletions apps/mobile/src/modules/discover/Recommendations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,66 @@ import { RSSHubCategories } from "@follow/constants"
import type { RSSHubRouteDeclaration } from "@follow/models/src/rsshub"
import { isASCII } from "@follow/utils"
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs"
import { useHeaderHeight } from "@react-navigation/elements"
import { FlashList } from "@shopify/flash-list"
import { useQuery } from "@tanstack/react-query"
import { useAtomValue } from "jotai"
import type { FC } from "react"
import { memo, useCallback, useMemo, useRef } from "react"
import { Text, TouchableOpacity, View } from "react-native"
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
import type { ScrollView } from "react-native"
import { Animated, Text, TouchableOpacity, useWindowDimensions, View } from "react-native"
import type { PanGestureHandlerGestureEvent } from "react-native-gesture-handler"
import { PanGestureHandler } from "react-native-gesture-handler"
import { useSafeAreaInsets } from "react-native-safe-area-context"

import { AnimatedScrollView } from "@/src/components/common/AnimatedComponents"
import type { TabComponent } from "@/src/components/ui/tabview/TabView"
import { TabView } from "@/src/components/ui/tabview/TabView"
import { apiClient } from "@/src/lib/api-fetch"

import { RSSHubCategoryCopyMap } from "./copy"
import { DiscoverContext } from "./DiscoverContext"
import { RecommendationListItem } from "./RecommendationListItem"

export const Recommendations = () => {
const headerHeight = useHeaderHeight()
const { animatedX, currentTabAtom } = useContext(DiscoverContext)
const currentTab = useAtomValue(currentTabAtom)

const windowWidth = useWindowDimensions().width
const contentScrollerRef = useRef<ScrollView>(null)

useEffect(() => {
contentScrollerRef.current?.scrollTo({ x: currentTab * windowWidth, y: 0, animated: true })
}, [currentTab, windowWidth])

const [loadedTabIndex, setLoadedTabIndex] = useState(() => new Set())
useEffect(() => {
setLoadedTabIndex((prev) => {
prev.add(currentTab)
return new Set(prev)
})
}, [currentTab])
return (
<TabView
lazyOnce
lazyTab
Tab={Tab}
tabbarStyle={{ paddingTop: headerHeight }}
tabs={RSSHubCategories.map((category) => ({
name: RSSHubCategoryCopyMap[category],
value: category,
}))}
/>
<AnimatedScrollView
onScroll={Animated.event([{ nativeEvent: { contentOffset: { x: animatedX } } }], {
useNativeDriver: true,
})}
ref={contentScrollerRef}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
nestedScrollEnabled
>
{RSSHubCategories.map((category, index) => (
<View className="flex-1" style={{ width: windowWidth }} key={category}>
{loadedTabIndex.has(index) && (
<Tab
key={category}
tab={{ name: RSSHubCategoryCopyMap[category], value: category }}
isSelected={currentTab === index}
/>
)}
</View>
))}
</AnimatedScrollView>
)
}

Expand Down Expand Up @@ -145,6 +175,10 @@ const Tab: TabComponent = ({ tab, ...rest }) => {
return typeof item === "string" ? item : item.key
}, [])

const { headerHeightAtom } = useContext(DiscoverContext)
const headerHeight = useAtomValue(headerHeightAtom)

const insets = useSafeAreaInsets()
if (isLoading) {
return null
}
Expand All @@ -158,8 +192,12 @@ const Tab: TabComponent = ({ tab, ...rest }) => {
keyExtractor={keyExtractor}
getItemType={getItemType}
renderItem={ItemRenderer}
scrollIndicatorInsets={{ right: -2 }}
contentContainerStyle={{ paddingBottom: tabHeight }}
scrollIndicatorInsets={{
right: -2,
top: headerHeight - insets.top,
bottom: tabHeight - insets.bottom,
}}
contentContainerStyle={{ paddingBottom: tabHeight, paddingTop: headerHeight }}
removeClippedSubviews
/>
{/* Right Sidebar */}
Expand Down Expand Up @@ -244,8 +282,15 @@ const NavigationSidebar: FC<{
[scrollToLetter, titles],
)

const { headerHeightAtom } = useContext(DiscoverContext)
const headerHeight = useAtomValue(headerHeightAtom)
const tabHeight = useBottomTabBarHeight()

return (
<View className="absolute inset-y-0 right-1 h-full items-center justify-center">
<View
className="absolute inset-y-0 right-1 h-full items-center justify-center"
style={{ paddingTop: headerHeight, paddingBottom: tabHeight }}
>
<PanGestureHandler onGestureEvent={handleGesture}>
<View className="gap-0.5">
{titles.map((title) => (
Expand Down
28 changes: 26 additions & 2 deletions apps/mobile/src/modules/discover/search.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { RSSHubCategories } from "@follow/constants"
import { getDefaultHeaderHeight } from "@react-navigation/elements"
import { router } from "expo-router"
import { useAtom, useAtomValue, useSetAtom } from "jotai"
import type { FC } from "react"
import { useEffect, useRef, useState } from "react"
import { useContext, useEffect, useRef, useState } from "react"
import type { LayoutChangeEvent } from "react-native"
import {
Animated,
Expand All @@ -18,10 +19,13 @@ import {
import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-context"

import { BlurEffect } from "@/src/components/common/BlurEffect"
import { TabBar } from "@/src/components/ui/tabview/TabBar"
import { Search2CuteReIcon } from "@/src/icons/search_2_cute_re"
import { accentColor, useColor } from "@/src/theme/colors"

import { RSSHubCategoryCopyMap } from "./copy"
import { useSearchPageContext } from "./ctx"
import { DiscoverContext } from "./DiscoverContext"
import { SearchTabBar } from "./SearchTabBar"

export const SearchHeader: FC<{
Expand Down Expand Up @@ -54,13 +58,33 @@ const DiscoverHeaderImpl = () => {
const frame = useSafeAreaFrame()
const insets = useSafeAreaInsets()
const headerHeight = getDefaultHeaderHeight(frame, false, insets.top)
const { animatedX, currentTabAtom, headerHeightAtom } = useContext(DiscoverContext)
const setCurrentTab = useSetAtom(currentTabAtom)
const setHeaderHeight = useSetAtom(headerHeightAtom)

return (
<View style={{ height: headerHeight, paddingTop: insets.top }} className="relative">
<View
style={{ minHeight: headerHeight, paddingTop: insets.top }}
className="relative"
onLayout={(e) => {
setHeaderHeight(e.nativeEvent.layout.height)
}}
>
<BlurEffect />
<View style={styles.header}>
<PlaceholerSearchBar />
</View>

<TabBar
tabs={RSSHubCategories.map((category) => ({
name: RSSHubCategoryCopyMap[category],
value: category,
}))}
tabScrollContainerAnimatedX={animatedX}
onTabItemPress={(index) => {
setCurrentTab(index)
}}
/>
</View>
)
}
Expand Down
16 changes: 9 additions & 7 deletions apps/mobile/src/modules/settings/routes/General.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ export const GeneralScreen = () => {

<GroupedInsetListBaseCell>
<Text>Translation Language</Text>
<Select
value={translationLanguage}
onValueChange={(value) => {
setGeneralSetting("translationLanguage", value)
}}
options={Object.values(LanguageMap)}
/>
<View className="w-[180px]">
<Select
value={translationLanguage}
onValueChange={(value) => {
setGeneralSetting("translationLanguage", value)
}}
options={Object.values(LanguageMap)}
/>
</View>
</GroupedInsetListBaseCell>
</GroupedInsetListCard>
</View>
Expand Down
23 changes: 20 additions & 3 deletions apps/mobile/src/screens/(stack)/(tabs)/discover.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
import { Stack } from "expo-router"
import { atom } from "jotai"
import { useMemo, useState } from "react"
import { useAnimatedValue } from "react-native"

import { DiscoverContext } from "@/src/modules/discover/DiscoverContext"
import { Recommendations } from "@/src/modules/discover/Recommendations"
import { DiscoverHeader } from "@/src/modules/discover/search"

export default function Discover() {
const animatedX = useAnimatedValue(0)
const currentTabAtom = useState(() => atom(0))[0]
const headerHeightAtom = useState(() => atom(0))[0]
const ctxValue = useMemo(
() => ({ animatedX, currentTabAtom, headerHeightAtom }),
[animatedX, currentTabAtom, headerHeightAtom],
)
return (
<>
<DiscoverContext.Provider value={ctxValue}>
<Stack.Screen
options={{
headerShown: true,
headerTransparent: true,

header: DiscoverHeader,
header: () => {
return (
<DiscoverContext.Provider value={ctxValue}>
<DiscoverHeader />
</DiscoverContext.Provider>
)
},
}}
/>

<Recommendations />
</>
</DiscoverContext.Provider>
)
}

0 comments on commit 7d98e27

Please sign in to comment.