import { Colors } from '@/constants/Colors'; import React, { memo, useEffect, useMemo, useRef, useState } from 'react'; import { Animated, Easing, LayoutChangeEvent, StyleSheet, Text, View, ViewStyle } from 'react-native'; type ProgressBarProps = { progress: number; // 0 - 1 height?: number; style?: ViewStyle; trackColor?: string; fillColor?: string; animated?: boolean; showLabel?: boolean; }; function ProgressBarImpl({ progress, height = 18, style, trackColor = '#EDEDED', fillColor = Colors.light.accentGreen, animated = true, showLabel = true, }: ProgressBarProps) { const [trackWidth, setTrackWidth] = useState(0); const animatedValue = useRef(new Animated.Value(0)).current; const clamped = useMemo(() => { if (Number.isNaN(progress)) return 0; return Math.min(1, Math.max(0, progress)); }, [progress]); useEffect(() => { if (!animated) { animatedValue.setValue(clamped); return; } Animated.timing(animatedValue, { toValue: clamped, duration: 650, easing: Easing.out(Easing.cubic), useNativeDriver: false, }).start(); }, [clamped, animated, animatedValue]); const lastWidthRef = useRef(0); const onLayout = (e: LayoutChangeEvent) => { const w = e.nativeEvent.layout.width || 0; // 仅在宽度发生明显变化时才更新,避免渲染-布局循环 const next = Math.round(w); if (next > 0 && next !== lastWidthRef.current) { lastWidthRef.current = next; setTrackWidth(next); } }; const fillWidth = Animated.multiply(animatedValue, trackWidth || 1); const percent = Math.round(clamped * 100); return ( {showLabel && ( {percent}% )} ); } export const ProgressBar = memo(ProgressBarImpl, (prev, next) => { // 仅在这些关键属性变化时重新渲染,忽略 style 的引用变化 return ( prev.progress === next.progress && prev.height === next.height && prev.trackColor === next.trackColor && prev.fillColor === next.fillColor && prev.animated === next.animated && prev.showLabel === next.showLabel ); }); const styles = StyleSheet.create({ container: { width: '100%', borderRadius: 10, overflow: 'hidden', backgroundColor: 'transparent', }, track: { ...StyleSheet.absoluteFillObject, borderRadius: 10, }, fill: { height: '100%', borderRadius: 10, justifyContent: 'center', paddingHorizontal: 10, }, label: { color: '#192126', fontWeight: '700', fontSize: 12, }, });