React Native实现圆形进度条

React Native实现自定义的圆形进度条动画,主要需要用到Animated和react native svg这个插件。先看看最终实现的效果:

大致思路如下:

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>

上面代码中使用<G>标签包裹两个外层Circle,主要是为了逆时针旋转Circle 90度。默认Circle使用strokeDashoffset绘制偏移量时的起点是时钟的3点钟位置,这样通过逆时针旋转90度,可以修改起点至12点钟位置。绘制圆形进度条的核心属性就是这个strokeDashoffset,通过定时改变绘制偏移量从而产生进度条的效果。所以上面代码中的circumferenceWithProgress是一个Animated动画值。

3. 添加动画属性

 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);  // 重置动画值
  }; 

4. 添加内层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;

 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值