Files
llink/js/mobile/src/features/stream-view/PlaybackPageIndicator.tsx
T

102 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect } from "react";
import { View } from "react-native";
import Animated, {
Easing,
cancelAnimation,
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
interface PlaybackPageIndicatorProps {
total: number;
current: number;
/** 01 progress for the active segment. Source ticks at ~100ms. */
progress: number;
paused: boolean;
}
const SEGMENT_GAP = 3;
const SEGMENT_HEIGHT = 2.5;
const SMOOTHING_MS = 300;
/**
* Snapchat-style segmented progress bar. Past segments full, future empty,
* active segment animated. The 300ms linear smoothing absorbs the 100ms
* tick from the particle view source so motion looks continuous at 60fps.
*/
export function PlaybackPageIndicator({
total,
current,
progress,
paused,
}: PlaybackPageIndicatorProps) {
if (total === 0) return null;
return (
<View className="flex-row items-stretch" style={{ gap: SEGMENT_GAP }}>
{Array.from({ length: total }).map((_, i) => (
<Segment
key={i}
isActive={i === current}
isPast={i < current}
progress={progress}
paused={paused}
/>
))}
</View>
);
}
interface SegmentProps {
isActive: boolean;
isPast: boolean;
progress: number;
paused: boolean;
}
function Segment({ isActive, isPast, progress, paused }: SegmentProps) {
// Each segment owns its own width animation. Past = 1, future = 0,
// active = animated toward `progress`. Reanimated keeps the tween on the
// UI thread so JS thread stalls (e.g. the 100ms text tick re-render)
// can't drop frames here.
const fill = useSharedValue(isPast ? 1 : 0);
useEffect(() => {
if (isPast) {
cancelAnimation(fill);
fill.value = withTiming(1, { duration: 120, easing: Easing.out(Easing.cubic) });
return;
}
if (!isActive) {
cancelAnimation(fill);
fill.value = 0;
return;
}
if (paused) {
cancelAnimation(fill);
return;
}
fill.value = withTiming(progress, {
duration: SMOOTHING_MS,
easing: Easing.linear,
});
}, [isPast, isActive, progress, paused, fill]);
const fillStyle = useAnimatedStyle(() => ({
width: `${Math.min(Math.max(fill.value, 0), 1) * 100}%`,
}));
return (
<View
className="flex-1 overflow-hidden rounded-full bg-white/30"
style={{ height: SEGMENT_HEIGHT }}
>
<Animated.View
className="h-full bg-white/95 rounded-full"
style={fillStyle}
/>
</View>
);
}