feat(mobile): integrate expo-glass-effect for enhanced blur effects

- Added the `expo-glass-effect` package to `package.json` to enable glass effect features.
- Updated `BlurEffect` and `ThemedBlurView` components to conditionally render a glass effect on iOS 26 devices.
- Introduced `GlassPlayerTabBar` component for improved tab bar aesthetics and functionality.
- Enhanced `PlayerAction` component to utilize the new glass effect in its UI.

These changes aim to improve the visual experience of the mobile application by incorporating modern UI effects.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-09-24 23:53:37 +08:00
parent 36a86d48a7
commit 822d6b0417
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
7 changed files with 142 additions and 29 deletions

View File

@ -65,6 +65,7 @@
"expo-device": "7.1.4",
"expo-document-picker": "13.1.6",
"expo-file-system": "18.1.10",
"expo-glass-effect": "0.1.4",
"expo-haptics": "14.1.4",
"expo-iap": "2.5.0",
"expo-image": "2.3.2",

View File

@ -1,6 +1,8 @@
import { GlassView } from "expo-glass-effect"
import { StyleSheet } from "react-native"
import { ThemedBlurView } from "@/src/components/common/ThemedBlurView"
import { isIos26 } from "@/src/lib/platform"
const node = (
<ThemedBlurView
@ -11,6 +13,17 @@ const node = (
}}
/>
)
const glassNode = (
<GlassView
style={{
...StyleSheet.absoluteFillObject,
overflow: "hidden",
backgroundColor: "transparent",
}}
glassEffectStyle="regular"
/>
)
export const BlurEffect = () => {
return node
return isIos26 ? glassNode : node
}

View File

@ -1,9 +1,12 @@
import type { BlurViewProps } from "expo-blur"
import { BlurView } from "expo-blur"
import { GlassView } from "expo-glass-effect"
import { useColorScheme } from "nativewind"
import { Platform, StyleSheet, View } from "react-native"
import { useColor } from "react-native-uikit-colors"
import { isIos26 } from "@/src/lib/platform"
/**
* @android In Android, the BlurView is experimental and not fully supported,
* so we use a normal View with a background color with **100% opacity**.
@ -13,14 +16,18 @@ import { useColor } from "react-native-uikit-colors"
export const ThemedBlurView = ({
ref,
tint,
useGlass,
...rest
}: BlurViewProps & { ref?: React.Ref<BlurView | null> }) => {
}: BlurViewProps & { ref?: React.Ref<BlurView | null>; useGlass?: boolean }) => {
const { colorScheme } = useColorScheme()
const background = useColor("systemBackground")
const useBlurView = Platform.OS === "ios" || "experimentalBlurMethod" in rest
if (isIos26 && useGlass) {
return <GlassView style={rest.style} glassEffectStyle="regular" />
}
return useBlurView ? (
<BlurView
ref={ref}

View File

@ -1,10 +1,15 @@
import { Portal } from "@gorhom/portal"
import { use, useLayoutEffect } from "react"
import { Platform } from "react-native"
import { Platform, View } from "react-native"
import DeviceInfo from "react-native-device-info"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { BottomTabContext } from "@/src/lib/navigation/bottom-tab/BottomTabContext"
import { TabBarPortal } from "@/src/lib/navigation/bottom-tab/TabBarPortal"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { NavigationInstanceContext } from "@/src/lib/navigation/NavigationInstanceContext"
import { isIos26 } from "@/src/lib/platform"
import { GlassPlayerTabBar } from "@/src/modules/player/GlassPlayerTabBar"
import { BottomTabs } from "./BottomTabs"
import { SetBottomTabBarHeightContext } from "./contexts/BottomTabBarHeightContext"
@ -25,10 +30,24 @@ export const ReactNativeTab = () => {
const NativeTabBarHolder = () => {
const setHeight = use(SetBottomTabBarHeightContext)
const insets = useSafeAreaInsets()
const tabBarHeight = 68 + insets.bottom
useLayoutEffect(() => {
//https://developer.apple.com/design/human-interface-guidelines/tab-bars
setHeight(68 + insets.bottom)
}, [insets.bottom, setHeight])
setHeight(tabBarHeight)
}, [insets.bottom, setHeight, tabBarHeight])
return null
const bottomTabContext = use(BottomTabContext)
const navigation = useNavigation()
return (
<Portal>
<BottomTabContext value={bottomTabContext}>
<NavigationInstanceContext value={navigation}>
<View className="bg-red absolute inset-x-0 bottom-[68px] mb-4">
<GlassPlayerTabBar className="absolute inset-x-0 bottom-0" />
</View>
</NavigationInstanceContext>
</BottomTabContext>
</Portal>
)
}

View File

@ -1,41 +1,44 @@
import Constants from "expo-constants"
import { Platform } from "react-native"
type Layout = { width: number; height: number }
import { isIOS } from "@/src/lib/platform"
/**
* @description In order to make android header height same as ios, we need to custom this function.
* @copyright copy from @react-navigation/elements/src/Header/getDefaultHeaderHeight.tsx
*/
export function getDefaultHeaderHeight(
layout: Layout,
modalPresentation: boolean,
topInset: number,
): number {
let headerHeight = 0
export function getDefaultHeaderHeight({
landscape,
modalPresentation,
}: {
landscape: boolean
modalPresentation: boolean
topInset: number
}): number {
let headerHeight
const { statusBarHeight } = Constants
// On models with Dynamic Island the status bar height is smaller than the safe area top inset.
const hasDynamicIsland = topInset > 50
const topHeight = hasDynamicIsland ? Constants.statusBarHeight : topInset
const hasDynamicIsland = isIOS && statusBarHeight > 50
const isLandscape = layout.width > layout.height
if (Platform.OS === "ios" && Platform.isPad) {
if (modalPresentation) {
headerHeight = 56
} else {
headerHeight = 50
}
} else {
if (isLandscape) {
headerHeight = 32
} else {
if (Platform.OS === "ios") {
if (Platform.isPad || Platform.isTV) {
if (modalPresentation) {
headerHeight = 56
} else {
headerHeight = 44
headerHeight = 50
}
} else {
if (modalPresentation && !landscape) {
headerHeight = 56
} else {
headerHeight = hasDynamicIsland ? 50 : 44
}
}
} else {
headerHeight = 64
}
return headerHeight + (!modalPresentation ? topHeight : 0)
return headerHeight + statusBarHeight
}

View File

@ -53,6 +53,7 @@ export function PlayerAction({
>
<View className={cn("overflow-hidden rounded-full p-2", buttonClassName)}>
<ThemedBlurView
useGlass
style={StyleSheet.absoluteFillObject}
intensity={30}
experimentalBlurMethod="none"

View File

@ -0,0 +1,69 @@
import { clsx } from "@follow/utils"
import { GlassView } from "expo-glass-effect"
import { useAtomValue } from "jotai"
import { use } from "react"
import { Pressable, StyleSheet, View } from "react-native"
import { Image } from "@/src/components/ui/image/Image"
import { Text } from "@/src/components/ui/typography/Text"
import { BottomTabContext } from "@/src/lib/navigation/bottom-tab/BottomTabContext"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { useActiveTrack } from "@/src/lib/player"
import { PlayerScreen } from "@/src/screens/PlayerScreen"
import { usePrefetchImageColors } from "@/src/store/image/hooks"
import { PlayPauseButton, SeekButton } from "./control"
const allowedTabIdentifiers = new Set(["IndexTabScreen", "SubscriptionsTabScreen"])
export function GlassPlayerTabBar({ className }: { className?: string }) {
const activeTrack = useActiveTrack()
const tabRootCtx = use(BottomTabContext)
const tabScreens = useAtomValue(tabRootCtx.tabScreensAtom)
const currentIndex = useAtomValue(tabRootCtx.currentIndexAtom)
const currentTabProps = tabScreens.find((tabScreen) => tabScreen.tabScreenIndex === currentIndex)
const identifier = currentTabProps?.identifier
const isVisible = !!activeTrack && identifier && allowedTabIdentifiers.has(identifier)
usePrefetchImageColors(activeTrack?.artwork)
const navigation = useNavigation()
if (!isVisible) return null
return (
<View className={clsx("mx-6", className)}>
<View className="my-6 h-[56px] flex-1">
<GlassView style={styles.glass} glassEffectStyle="regular" />
<Pressable
onPress={() => {
navigation.presentControllerView(PlayerScreen, void 0, "transparentModal")
}}
>
<View className="flex flex-row items-center gap-4 overflow-hidden rounded-2xl p-2 px-3">
<Image
source={{
uri: activeTrack?.artwork ?? "",
}}
className="size-12 rounded-full"
/>
<View className="flex-1 overflow-hidden">
<Text className="text-label text-lg font-semibold" numberOfLines={1}>
{activeTrack?.title ?? ""}
</Text>
</View>
<View className="mr-2 flex flex-row gap-4">
<PlayPauseButton />
<SeekButton />
</View>
</View>
</Pressable>
</View>
</View>
)
}
const styles = StyleSheet.create({
glass: {
borderRadius: 99,
...StyleSheet.absoluteFillObject,
},
})