直播代碼React Native實現圓形進度條

大致思路如下:

  1. 使用Svg創建畫布,指定畫布的寬高;

  2. 創建外層倒計時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>

上面代碼中使用標籤包裹兩個外層Circle,主要是爲了逆時針旋轉Circle 90度。默認Circle使用strokeDashoffset繪製偏移量時的起點是時鐘的3點鐘位置,這樣通過逆時針旋轉90度,可以修改起點至12點鐘位置。繪製圓形進度條的核心屬性就是這個strokeDashoffset,通過定時改變繪製偏移量從而產生進度條的效果。所以上面代碼中的circumferenceWithProgress是一個Animated動畫值。

  1. 添加動畫屬性
 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);  // 重置動畫值
  }; 
  1. 添加內層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;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章