mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 12:14:47 +02:00
make highlights not highlight the wrong thing
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user