mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 12:14:47 +02:00
minor UI/UX improvement
This commit is contained in:
+120
-16
@@ -1,9 +1,16 @@
|
||||
import { View, Pressable, StyleSheet } from 'react-native';
|
||||
import { useEffect } from 'react';
|
||||
import { View, Pressable, StyleSheet, type LayoutChangeEvent } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from './Text';
|
||||
import Animated, {
|
||||
interpolateColor,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
import { MiniPlayer } from './MiniPlayer';
|
||||
import { colors, layout, spacing } from '@/theme';
|
||||
import { colors, fonts, layout, spacing } from '@/theme';
|
||||
import { motion } from '@/theme/motion';
|
||||
|
||||
type IconName = keyof typeof Ionicons.glyphMap;
|
||||
|
||||
@@ -32,7 +39,34 @@ interface TabBarProps {
|
||||
*/
|
||||
export function TabBar({ items, onPress }: TabBarProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const tabs = items.filter((item) => TAB_META[item.name]);
|
||||
const homeFocused = items.some((item) => item.name === 'index' && item.focused);
|
||||
const count = tabs.length;
|
||||
const activeIndex = Math.max(
|
||||
0,
|
||||
tabs.findIndex((item) => item.focused),
|
||||
);
|
||||
|
||||
// The "playhead": a single accent bar that glides along the top edge to the
|
||||
// active tab, travelling in the same direction as the scene transition.
|
||||
const barWidth = useSharedValue(0);
|
||||
const position = useSharedValue(activeIndex);
|
||||
|
||||
useEffect(() => {
|
||||
position.value = withTiming(activeIndex, motion.snap);
|
||||
}, [activeIndex, position]);
|
||||
|
||||
const indicatorStyle = useAnimatedStyle(() => {
|
||||
const segment = count > 0 ? barWidth.value / count : 0;
|
||||
return {
|
||||
width: segment,
|
||||
transform: [{ translateX: position.value * segment }],
|
||||
};
|
||||
});
|
||||
|
||||
const onBarLayout = (e: LayoutChangeEvent) => {
|
||||
barWidth.value = e.nativeEvent.layout.width;
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.wrap}>
|
||||
@@ -42,25 +76,21 @@ export function TabBar({ items, onPress }: TabBarProps) {
|
||||
styles.bar,
|
||||
{ paddingBottom: insets.bottom, height: layout.tabBarHeight + insets.bottom },
|
||||
]}
|
||||
onLayout={onBarLayout}
|
||||
>
|
||||
{items.map((item) => {
|
||||
<Animated.View style={[styles.indicator, indicatorStyle]} pointerEvents="none">
|
||||
<View style={styles.indicatorBar} />
|
||||
</Animated.View>
|
||||
{tabs.map((item) => {
|
||||
const meta = TAB_META[item.name];
|
||||
if (!meta) return null;
|
||||
const color = item.focused ? colors.accent : colors.textTertiary;
|
||||
return (
|
||||
<Pressable
|
||||
<TabButton
|
||||
key={item.key}
|
||||
style={styles.tab}
|
||||
meta={meta}
|
||||
focused={item.focused}
|
||||
onPress={() => onPress(item)}
|
||||
hitSlop={8}
|
||||
accessibilityRole="tab"
|
||||
accessibilityState={{ selected: item.focused }}
|
||||
>
|
||||
<Ionicons name={meta.icon} size={22} color={color} />
|
||||
<Text variant="caption" style={[styles.label, { color }]}>
|
||||
{meta.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
@@ -68,6 +98,67 @@ export function TabBar({ items, onPress }: TabBarProps) {
|
||||
);
|
||||
}
|
||||
|
||||
interface TabButtonProps {
|
||||
meta: { label: string; icon: IconName };
|
||||
focused: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single tab. Selecting it cross-fades the icon/label colour to accent;
|
||||
* pressing depresses the icon for tactile feedback. Reanimated only drives
|
||||
* opacity/transform on plain Animated.Views — @expo/vector-icons' Icon is a
|
||||
* class-wrapped Text that Animated can't drive, so colour is a cross-fade
|
||||
* between a grey base icon and an accent one stacked on top. Spring-free per
|
||||
* theme/motion.
|
||||
*/
|
||||
function TabButton({ meta, focused, onPress }: TabButtonProps) {
|
||||
// 0 = inactive, 1 = active. Drives the accent fill, label colour, and bloom.
|
||||
const progress = useSharedValue(focused ? 1 : 0);
|
||||
// 0 = at rest, 1 = finger down.
|
||||
const press = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
progress.value = withTiming(focused ? 1 : 0, motion.quick);
|
||||
}, [focused, progress]);
|
||||
|
||||
const depressStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: 1 - press.value * 0.12 }],
|
||||
}));
|
||||
const accentStyle = useAnimatedStyle(() => ({ opacity: progress.value }));
|
||||
const labelStyle = useAnimatedStyle(() => ({
|
||||
color: interpolateColor(
|
||||
progress.value,
|
||||
[0, 1],
|
||||
[colors.textTertiary, colors.accent],
|
||||
),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.tab}
|
||||
onPress={onPress}
|
||||
onPressIn={() => {
|
||||
press.value = withTiming(1, motion.quick);
|
||||
}}
|
||||
onPressOut={() => {
|
||||
press.value = withTiming(0, motion.quick);
|
||||
}}
|
||||
hitSlop={8}
|
||||
accessibilityRole="tab"
|
||||
accessibilityState={{ selected: focused }}
|
||||
>
|
||||
<Animated.View style={depressStyle}>
|
||||
<Ionicons name={meta.icon} size={22} color={colors.textTertiary} />
|
||||
<Animated.View style={[StyleSheet.absoluteFill, accentStyle]}>
|
||||
<Ionicons name={meta.icon} size={22} color={colors.accent} />
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
<Animated.Text style={[styles.label, labelStyle]}>{meta.label}</Animated.Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrap: {
|
||||
backgroundColor: colors.bgSecondary,
|
||||
@@ -78,6 +169,18 @@ const styles = StyleSheet.create({
|
||||
borderTopColor: colors.glassBorder,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
indicator: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
alignItems: 'center',
|
||||
},
|
||||
indicatorBar: {
|
||||
width: 28,
|
||||
height: 3,
|
||||
borderRadius: 999,
|
||||
backgroundColor: colors.accent,
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
@@ -87,6 +190,7 @@ const styles = StyleSheet.create({
|
||||
label: {
|
||||
marginTop: 2,
|
||||
fontSize: 10,
|
||||
fontFamily: fonts.sans.regular,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user