大致思路如下:
-
使用Svg創建畫布,指定畫布的寬高;
-
創建外層倒計時Circle,這裏需要使用兩個完全重合的Circle疊起來實現,這兩個Circle都只保留邊框,其中一個Circle顯示爲進度條背景色(上圖中的灰色),另一個Circle顯示爲進度條的前景色(上圖的綠色)。而且由於Svg採用的是渲染疊加圖層的方式,所以下層的圖形會疊在上層圖形之上。因此作爲背景的Circle需要放置在上方。
const outerCircleCommonConfig = {
fill: 'none', // 填充色爲空
cx: halfOfSvgSize, // 圓心x座標值(相對於Svg畫布)
cy: halfOfSvgSize, // 圓心y座標值(相對於Svg畫布)
r: radius, // 圓半徑
strokeWidth: strokeWidth, // 圓邊框寬度
strokeDasharray: `${circumference} ${circumference}` // 繪製圓邊點劃線的圖案範式,說白了就是定義虛線的渲染形式,circumference是整個圓的周長
};
<Svg width={svgSize} height={svgSize}>
{/* 外層倒計時圓圈 */}
<G rotation={-90} origin={`${halfOfSvgSize}, ${halfOfSvgSize}`}>
<Circle
stroke='#D2D2D2'
{...outerCircleCommonConfig}
/>
<Circle
stroke='#25BB7E'
{...outerCircleCommonConfig}
strokeDashoffset={circumferenceWithProgress}
/>
</G>
</Svg>
上面代碼中使用
- 添加動畫屬性
const { progress, durationTime } = props;
const radian = progress.interpolate({
inputRange: [0, 1],
outputRange: [0, 2 * Math.PI]
});
const circumferenceWithProgress = Animated.multiply(radius, radian);
const AnimatedCircleProgress = Animated.createAnimatedComponent(Circle);
爲了讓父級組件能夠靈活的控制進度條,所以這裏將progress和durationTime(持續時間)定義在父級:
const durationTime = 5; // 持續時間(單位:s)
const [progress, setProgress] = useState(new Animated.Value(0)); // 倒計時動畫進度
<CircleProgress progress={progress} durationTime={durationTime}/>
回到CircleProgress組件中,這裏將progress動畫值使用interpolate做了一層動畫值的映射(插值),我們的進度一般都是[0, 1],而strokeDashOffset偏移量應該是[0, 2 * Math.PI] * radius(弧度 * 半徑)範圍內。最後爲了保證外層Circle的stroke能夠“動起來”,需要使用Animated.createAnimatedComponent()方法創建一個能執行動畫效果的Circle組件AnimatedCircleProgress。修改後如下:
<Svg width={svgSize} height={svgSize}>
{/* 外層倒計時圓圈 */}
<G rotation={-90} origin={`${halfOfSvgSize}, ${halfOfSvgSize}`}>
<Circle
stroke='#D2D2D2'
{...outerCircleCommonConfig}
/>
<AnimatedCircleProgress
stroke='#25BB7E'
{...outerCircleCommonConfig}
strokeDashoffset={circumferenceWithProgress}
/>
</G>
</Svg>
之後在父級組件中,調用如下方法,修改progress動畫值即可。
// 開始倒計時
const startCountdown = () => {
Animated.timing(progress, {
toValue: 1,
duration: durationTime * 1000,
easing: Easing.linear
}).start(() => {});
};
如果我們想停止並重置進度條,可以調用如下方法:
const resetCountDown = () => {
progress.stopAnimation(); // 停止當前動畫
progress.setValue(0); // 重置動畫值
};
- 添加內層Circle,並顯示倒計時時間,還是在CircleProgress組件中:
const [count, setCount] = useState(durationTime);
useEffect(() => {
progress.addListener(({ value }) => {
const ratio = 1 - value;
setCount(Math.round(durationTime * ratio));
});
return () => {
progress.removeAllListeners();
}
}, []);
<Svg width={svgSize} height={svgSize}>
{/* 內層顯示倒計時時間圓圈 */}
<Circle
stroke='#25BB7E'
fill='#25BB7E'
cx={halfOfSvgSize}
cy={halfOfSvgSize}
r={innerRadius}
strokeWidth={strokeWidth}
strokeDasharray={`${circumference} ${circumference}`}
/>
<Text
fill="#fff"
fontSize="20"
fontWeight="bold"
x={halfOfSvgSize}
y={halfOfSvgSize + 5}
textAnchor="middle"
>
{`${count} s`}
</Text>
</Svg>
這裏使用progress.addListener監聽動畫值progress的進度,並計算對應的倒計時的值。然後渲染在Text文本中。
最後我們只需要在父級組件中修改常量durationTime的值就能改變總倒計時時間。非常的方便。
CircleProgress組件完整代碼如下:src/components/CirclProgress/index.js文件
/**
* 圓形進度條
*/
import React, { useState, useEffect } from 'react';
import { Animated } from 'react-native';
import Svg, { Circle, Text, G } from 'react-native-svg';
import { LogUtil } from '@utils';
const svgSize = 100; // 畫布的寬高
const halfOfSvgSize = svgSize / 2;
const strokeWidth = 2; // 圓形進度條寬度
const radius = (svgSize - strokeWidth) / 2; // 外層倒計時進度半徑
const innerRadius = radius - 6; // 內層半徑
const circumference = 2 * radius * Math.PI; // 總周長
const CircleProgress = (props) => {
const { progress, durationTime } = props;
const radian = progress.interpolate({
inputRange: [0, 1],
outputRange: [0, 2 * Math.PI]
});
const circumferenceWithProgress = Animated.multiply(radius, radian);
const AnimatedCircleProgress = Animated.createAnimatedComponent(Circle);
const outerCircleCommonConfig = {
fill: 'none',
cx: halfOfSvgSize,
cy: halfOfSvgSize,
r: radius,
strokeWidth: strokeWidth,
strokeDasharray: `${circumference} ${circumference}`
};
const [count, setCount] = useState(durationTime);
useEffect(() => {
progress.addListener(({ value }) => {
const ratio = 1 - value;
setCount(Math.round(durationTime * ratio));
});
return () => {
progress.removeAllListeners();
}
}, [])
return (
<Svg width={svgSize} height={svgSize}>
{/* 內層顯示倒計時時間圓圈 */}
<Circle
stroke='#25BB7E'
fill='#25BB7E'
cx={halfOfSvgSize}
cy={halfOfSvgSize}
r={innerRadius}
strokeWidth={strokeWidth}
strokeDasharray={`${circumference} ${circumference}`}
/>
<Text
fill="#fff"
fontSize="20"
fontWeight="bold"
x={halfOfSvgSize}
y={halfOfSvgSize + 5}
textAnchor="middle"
>
{`${count} s`}
</Text>
{/* 外層倒計時圓圈 */}
<G rotation={-90} origin={`${halfOfSvgSize}, ${halfOfSvgSize}`}>
<Circle
stroke='#D2D2D2'
{...outerCircleCommonConfig}
/>
<AnimatedCircleProgress
stroke='#25BB7E'
{...outerCircleCommonConfig}
strokeDashoffset={circumferenceWithProgress}
/>
</G>
</Svg>
);
};
export default CircleProgress;