欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

环形进度条的简单实现

程序员文章站 2024-03-21 11:35:46
...
小牢骚

好久没有记录文章了,近期在学习前端的相关知识,也没有好好的静下心来整理东西,这次准备把之前自己用到的东西做一个总结,方便以后自己查阅.

正文

这次记录使用CALayer创建一个环形进度条的view,加上简单的动画,项目中刚好有使用到。
效果图:
环形进度条的简单实现
要点: 主要使用layer的mask属性,mask属性可以让layer只显示遮罩层上面的路径(环形)

分析

在进度值为0的时候,有一个黄色的底色,然后进度条的颜色是渐变的,底色环形可以通过给view的layer添加一个带环形路径的CAShapeLayer作为mask,渐变色可以使用CAGrandientLayer创建一个渐变图层,然后创建一个带环形路径的CAShapeLayer,将其作为渐变图层的mask,这样就能达到预期的效果

相关知识点

  • CAGradientLayer
    创建渐变色layer,他是继承自CALayer的一个用于绘制多色图层的子类,主要用于设置的属性值为
    • @property(nullable, copy) NSArray *colors;
      设置需要加载的颜色数组(CGColorRef类型)
    • @property(nullable, copy) NSArray
#define DEGREES_TO_ANGLE(x) (M_PI * (x) / 180.0) // 将角度转为弧度
CAShapeLayer *layer = [CAShapeLayer layer];
    layer.frame = self.bounds;

    UIBezierPath *path =  [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2) radius:self.bounds.size.width / 2.5 startAngle:DEGREES_TO_ANGLE(-90) endAngle:DEGREES_TO_ANGLE(270) clockwise:YES]; //注意这里的半径,应该为宽度1/2-linewidth/2
    layer.lineWidth = self.lineWidth; //线的宽度
    layer.path = path.CGPath;
    layer.fillColor = [UIColor clearColor].CGColor; //设置填充颜色
    layer.strokeColor = [UIColor blackColor].CGColor; // 设置画笔颜色
    layer.lineCap = kCALineCapRound; // 设置线为圆角

其中需要注意的点:
- 这里的坐标系和数学中的坐标系是反的,简单的理解就是-90°等价数学坐标系中的90°,90°等价数学坐标系中的270°,所以如果希望进度条从顶部按照顺时针走,那么在设置UIBezierPath画圆弧的时候就应该为clockwise:yes(顺时针),startangle(开始弧度):(-90°对应的弧度),endangle:(270°对应的弧度),这样就是生成按照顺时针方向从-90°的点到270°的点之间的路径。
如下图片中不同的角度对应的效果图:
环形进度条的简单实现
- 在选择半径时,如果fram宽度为100,希望绘制一个宽度20紧靠边缘的环形,那么半径应该是50-20/2,并不是50-20。

代码解析

progressView.h

typedef NS_ENUM (NSInteger, WJStrokeType)//动画方式
{
    WJStrokeTypeStart, //开始
    WJStrokeTypeEnd //结束
};
//动画结束回调
typedef void(^ProgressEnd)();

@interface WJProgressView : UIView
/**
 *  线宽
 */
@property(nonatomic,assign)CGFloat lineWidth;

/**
 *  背景颜色
 */
@property(nonatomic,strong)UIColor *bgColor;

/**
 *  渐变色颜色组
 */
@property(nonatomic,strong)NSArray *colors;

/**
 *  颜色分割位置数组
 */
@property(nonatomic,strong)NSArray *locations;

/**
 *  隐藏百分比label
 */
- (void)hidePercent;

/**
 *  设置创建图层
 */
- (void)startProress;

/**
 *  更新progress
 *
 *  @param value progress Value
 */
- (void)reloadValue:(CGFloat)value;

/**
 *  进度动画
 *
 *  @param beginValue 开始value
 *  @param endValue   结束value
 *  @param duration   动画持续时间
 *  @param type       动画类型
 *  @param endBlock   动画结束回调
 */
- (void)setprogressFromValue:(CGFloat)beginValue endValue:(CGFloat)endValue duration:(CGFloat)duration strokeType:(WJStrokeType)type end:(ProgressEnd)endBlock;

.m

- (void)setprogressFromValue:(CGFloat)beginValue endValue:(CGFloat)endValue duration:(CGFloat)time strokeType:(WJStrokeType)type end:(ProgressEnd)endBlock
{
    if (beginValue >= 1.0) {
        endBlock();
        return;
    }
    _endValue = endValue;
    self.animationEnd = endBlock;
    NSString *animationName = @"strokeStart";
    if (type) {
        animationName = @"strokeEnd";
    }
    CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:animationName];
    pathAnimation.duration = time;
    pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    pathAnimation.fromValue = [NSNumber numberWithFloat:beginValue];
    pathAnimation.toValue = [NSNumber numberWithFloat:endValue];
    pathAnimation.autoreverses = NO;
    pathAnimation.delegate = self;
    pathAnimation.removedOnCompletion = NO;
    pathAnimation.fillMode = kCAFillModeForwards;
    [self.colorMaskLayer addAnimation:pathAnimation forKey:@"strokeAnimation"];

    [self creatTimerFromValue:beginValue endValue:endValue duration:time];
}

对于cCAShapeLayer,stokeEnd和strokestart属性都直接动画操作。
在动画过程中关于中间percentLabel的显示,使用定时器设置时间间隔实现变化。

/**
 创建定时器在layer animation过程中改变percent

 @param beginValue 初始值
 @param endValue 结束值
 @param time 时间间隔
 */
- (void)creatTimerFromValue:(CGFloat)beginValue endValue:(CGFloat)endValue duration:(CGFloat)time
{
    __block CGFloat beginPercent = beginValue;
    CGFloat timeInterval = 0.1;

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, 0), timeInterval*NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(self.timer, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            CGFloat difference = endValue - beginValue;
            CGFloat addValue = difference/time/(1/timeInterval);
            beginPercent += addValue;
            if (beginPercent >= endValue)  beginPercent = endValue;
            if (beginPercent >= 1.0)  beginPercent = 1.0f;
            [self reloadPercentLabelValue:beginPercent];
        });
    });
    dispatch_resume(_timer);
}

注意点:dispatch_source_t 的创建定一个属性值来保存。不要直接在方法中创建。
在动画结束后对timer进行销毁处理。

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    if (self.animationEnd) self.animationEnd();
    [self.colorMaskLayer removeAnimationForKey:@"strokeAnimation"];
    self.colorMaskLayer.strokeEnd = _endValue;
    if (self.timer) {
        dispatch_source_cancel(self.timer);
        self.timer = nil;
    }
}

End demo地址