import React, { forwardRef, useMemo, useRef, useState, useEffect } from 'react'; import { View, Text, Animated, Platform, Easing } from 'react-native'; import type { ViewStyle } from 'react-native'; import Svg, { Defs, LinearGradient, Stop, Path } from 'react-native-svg'; import { isObject } from '../utils/typeof'; import type { CircleProps, CircleStartPosition } from './type'; import { useThemeFactory } from '../Theme'; import { useUpdateEffect } from '../hooks'; import { createStyle } from './style'; const AnimatePath = Animated.createAnimatedComponent(Path); let uid = 0; const PERIMETER = 3140; const format = (rate: string | number) => Math.min(Math.max(+rate, 0), 100); const getPath = (clockwise: boolean, viewBoxSize: number) => { const sweepFlag = clockwise ? 1 : 0; return `M ${viewBoxSize / 2} ${ viewBoxSize / 2 } m 0, -500 a 500, 500 0 1, ${sweepFlag} 0, 1000 a 500, 500 0 1, ${sweepFlag} 0, -1000`; }; const ROTATE_ANGLE_MAP: Record = { top: 0, right: 90, bottom: 180, left: 270, }; const Circle = forwardRef((props, ref) => { const { styles, theme } = useThemeFactory(createStyle, { size: props.size }); const { style, clockwise = true, speed = 100, fill = 'none', strokeWidth = 40, startPosition = 'top', color = theme.circle_color, layerColor = theme.circle_layer_color, strokeLinecap = 'round', text, children, rate = 0, } = props; const id = `van-circle-${uid++}`; const currentRate = useRef(new Animated.Value(rate)).current; const rateAnimated = useRef(); const [nextRate, setNextRate] = useState(rate); const viewBoxSize = useMemo(() => +strokeWidth + 1000, [strokeWidth]); const path = useMemo(() => getPath(clockwise, viewBoxSize), [clockwise, viewBoxSize]); const svgStyle = useMemo(() => { const angleValue = ROTATE_ANGLE_MAP[startPosition]; if (angleValue) { return { transform: [{ rotate: `${angleValue}deg` }], }; } return {}; }, [startPosition]); useEffect(() => { currentRate.addListener(i => setNextRate(i.value)); return () => { currentRate.removeAllListeners(); }; }, []); useUpdateEffect(() => { const startRate = (currentRate as any)._value; const endRate = format(rate); const duration = Math.abs(((startRate - endRate) * 1000) / speed); rateAnimated.current && rateAnimated.current.stop(); rateAnimated.current = Animated.timing(currentRate, { toValue: endRate, duration, easing: Easing.linear, useNativeDriver: Platform.OS !== 'web', }); rateAnimated.current.start(); }, [rate]); // 渐变色 const renderGradient = () => { if (!color || !isObject(color)) { return null; } const Stops = Object.keys(color) .sort((a, b) => parseFloat(a) - parseFloat(b)) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore // eslint-disable-next-line react/no-array-index-key .map((key, index) => ); return ( {Stops} ); }; return ( {renderGradient()} {/* 轨道 */} {text ? {text} : children} ); }); Circle.displayName = 'Circle'; export default Circle;