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:
parent
36a86d48a7
commit
822d6b0417
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue