mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 04:06:43 +02:00
make highlights not highlight the wrong thing
This commit is contained in:
@@ -2,8 +2,7 @@ import { useEffect } from 'react';
|
||||
import {
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
View,
|
||||
type LayoutChangeEvent
|
||||
View
|
||||
} from 'react-native';
|
||||
import Animated, {
|
||||
interpolateColor,
|
||||
@@ -34,40 +33,15 @@ interface SegmentedControlProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Equal-width segmented control on the TabBar "playhead" pattern: one glass
|
||||
* track, a thumb that glides to the active segment, labels cross-fading to the
|
||||
* accent via interpolateColor on Animated.Text. Spring-free per theme/motion.
|
||||
* Equal-width segmented control with a selection pill anchored inside the
|
||||
* active segment. The structural tie to `focused` keeps the pill and content
|
||||
* selection synchronized; labels still cross-fade via Animated.Text.
|
||||
*/
|
||||
export function SegmentedControl({ segments, value, onChange }: SegmentedControlProps) {
|
||||
const styles = useStyles();
|
||||
const count = segments.length;
|
||||
const activeIndex = Math.max(
|
||||
0,
|
||||
segments.findIndex((segment) => segment.key === value),
|
||||
);
|
||||
|
||||
const trackWidth = useSharedValue(0);
|
||||
const position = useSharedValue(activeIndex);
|
||||
|
||||
useEffect(() => {
|
||||
position.value = withTiming(activeIndex, motion.snap);
|
||||
}, [activeIndex, position]);
|
||||
|
||||
const thumbStyle = useAnimatedStyle(() => {
|
||||
const segment = count > 0 ? (trackWidth.value - THUMB_INSET * 2) / count : 0;
|
||||
return {
|
||||
width: segment,
|
||||
transform: [{ translateX: position.value * segment }],
|
||||
};
|
||||
});
|
||||
|
||||
const onTrackLayout = (e: LayoutChangeEvent) => {
|
||||
trackWidth.value = e.nativeEvent.layout.width;
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.track} onLayout={onTrackLayout}>
|
||||
<Animated.View style={[styles.thumb, thumbStyle]} pointerEvents="none" />
|
||||
<View style={styles.track}>
|
||||
{segments.map((segment) => (
|
||||
<SegmentButton
|
||||
key={segment.key}
|
||||
@@ -121,6 +95,7 @@ function SegmentButton({
|
||||
accessibilityRole="tab"
|
||||
accessibilityState={{ selected: focused }}
|
||||
>
|
||||
{focused ? <View style={styles.thumb} pointerEvents="none" /> : null}
|
||||
<Animated.Text style={[styles.label, labelStyle]} numberOfLines={1}>
|
||||
{label}
|
||||
</Animated.Text>
|
||||
@@ -139,9 +114,10 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
},
|
||||
thumb: {
|
||||
position: 'absolute',
|
||||
top: THUMB_INSET,
|
||||
bottom: THUMB_INSET,
|
||||
left: THUMB_INSET,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundColor: colors.glassHighlight,
|
||||
borderColor: colors.accent,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
|
||||
@@ -2,8 +2,7 @@ import { useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
type LayoutChangeEvent
|
||||
StyleSheet
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
@@ -53,32 +52,6 @@ export function TabBar({ items, onPress }: TabBarProps) {
|
||||
const styles = useStyles();
|
||||
const insets = useSafeAreaInsets();
|
||||
const tabs = items.filter((item) => TAB_META[item.name]);
|
||||
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}>
|
||||
@@ -88,11 +61,7 @@ export function TabBar({ items, onPress }: TabBarProps) {
|
||||
styles.bar,
|
||||
{ paddingBottom: insets.bottom, height: layout.tabBarHeight + insets.bottom },
|
||||
]}
|
||||
onLayout={onBarLayout}
|
||||
>
|
||||
<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;
|
||||
@@ -169,6 +138,11 @@ function TabButton({ meta, focused, onPress }: TabButtonProps) {
|
||||
accessibilityRole="tab"
|
||||
accessibilityState={{ selected: focused }}
|
||||
>
|
||||
{focused ? (
|
||||
<View style={styles.indicator} pointerEvents="none">
|
||||
<View style={styles.indicatorBar} />
|
||||
</View>
|
||||
) : null}
|
||||
<Animated.View style={depressStyle}>
|
||||
<Ionicons name={meta.icon} size={22} color={colors.textTertiary} />
|
||||
<Animated.View style={[StyleSheet.absoluteFill, accentStyle]}>
|
||||
@@ -194,6 +168,7 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
},
|
||||
indicatorBar: {
|
||||
|
||||
Reference in New Issue
Block a user