make highlights not highlight the wrong thing

This commit is contained in:
Boof2015
2026-07-25 17:23:08 -04:00
parent 0bde96e321
commit aaadff107c
2 changed files with 17 additions and 66 deletions
+10 -34
View File
@@ -2,8 +2,7 @@ import { useEffect } from 'react';
import { import {
Pressable, Pressable,
StyleSheet, StyleSheet,
View, View
type LayoutChangeEvent
} from 'react-native'; } from 'react-native';
import Animated, { import Animated, {
interpolateColor, interpolateColor,
@@ -34,40 +33,15 @@ interface SegmentedControlProps {
} }
/** /**
* Equal-width segmented control on the TabBar "playhead" pattern: one glass * Equal-width segmented control with a selection pill anchored inside the
* track, a thumb that glides to the active segment, labels cross-fading to the * active segment. The structural tie to `focused` keeps the pill and content
* accent via interpolateColor on Animated.Text. Spring-free per theme/motion. * selection synchronized; labels still cross-fade via Animated.Text.
*/ */
export function SegmentedControl({ segments, value, onChange }: SegmentedControlProps) { export function SegmentedControl({ segments, value, onChange }: SegmentedControlProps) {
const styles = useStyles(); 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 ( return (
<View style={styles.track} onLayout={onTrackLayout}> <View style={styles.track}>
<Animated.View style={[styles.thumb, thumbStyle]} pointerEvents="none" />
{segments.map((segment) => ( {segments.map((segment) => (
<SegmentButton <SegmentButton
key={segment.key} key={segment.key}
@@ -121,6 +95,7 @@ function SegmentButton({
accessibilityRole="tab" accessibilityRole="tab"
accessibilityState={{ selected: focused }} accessibilityState={{ selected: focused }}
> >
{focused ? <View style={styles.thumb} pointerEvents="none" /> : null}
<Animated.Text style={[styles.label, labelStyle]} numberOfLines={1}> <Animated.Text style={[styles.label, labelStyle]} numberOfLines={1}>
{label} {label}
</Animated.Text> </Animated.Text>
@@ -139,9 +114,10 @@ const useStyles = createThemedStyles((colors) => ({
}, },
thumb: { thumb: {
position: 'absolute', position: 'absolute',
top: THUMB_INSET, top: 0,
bottom: THUMB_INSET, right: 0,
left: THUMB_INSET, bottom: 0,
left: 0,
backgroundColor: colors.glassHighlight, backgroundColor: colors.glassHighlight,
borderColor: colors.accent, borderColor: colors.accent,
borderWidth: StyleSheet.hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
+7 -32
View File
@@ -2,8 +2,7 @@ import { useEffect } from 'react';
import { import {
View, View,
Pressable, Pressable,
StyleSheet, StyleSheet
type LayoutChangeEvent
} from 'react-native'; } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
@@ -53,32 +52,6 @@ export function TabBar({ items, onPress }: TabBarProps) {
const styles = useStyles(); const styles = useStyles();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const tabs = items.filter((item) => TAB_META[item.name]); 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 ( return (
<View style={styles.wrap}> <View style={styles.wrap}>
@@ -88,11 +61,7 @@ export function TabBar({ items, onPress }: TabBarProps) {
styles.bar, styles.bar,
{ paddingBottom: insets.bottom, height: layout.tabBarHeight + insets.bottom }, { 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) => { {tabs.map((item) => {
const meta = TAB_META[item.name]; const meta = TAB_META[item.name];
if (!meta) return null; if (!meta) return null;
@@ -169,6 +138,11 @@ function TabButton({ meta, focused, onPress }: TabButtonProps) {
accessibilityRole="tab" accessibilityRole="tab"
accessibilityState={{ selected: focused }} accessibilityState={{ selected: focused }}
> >
{focused ? (
<View style={styles.indicator} pointerEvents="none">
<View style={styles.indicatorBar} />
</View>
) : null}
<Animated.View style={depressStyle}> <Animated.View style={depressStyle}>
<Ionicons name={meta.icon} size={22} color={colors.textTertiary} /> <Ionicons name={meta.icon} size={22} color={colors.textTertiary} />
<Animated.View style={[StyleSheet.absoluteFill, accentStyle]}> <Animated.View style={[StyleSheet.absoluteFill, accentStyle]}>
@@ -194,6 +168,7 @@ const useStyles = createThemedStyles((colors) => ({
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
right: 0,
alignItems: 'center', alignItems: 'center',
}, },
indicatorBar: { indicatorBar: {