You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于CAShapeLayer与CABasicAnimation的环形进度条连续动画问题

解决CAShapeLayer环形动画连续调用仅显示最后一次的问题

嘿,这个问题我之前做进度环的时候也踩过坑!核心原因是你连续调用animateCircle时,第二次调用会直接覆盖掉第一次的动画设置——因为Core Animation里,当你直接修改strokeEnd这种图层属性时,会立刻更新模型层的值,同时新添加的动画会替换掉同名key的旧动画(你用了同一个@"animateCircle"作为key),导致第一次动画还没来得及执行就被冲掉了。

下面给你两种靠谱的解决方案,让动画能依次连贯执行:

方案一:给动画添加完成回调,链式执行

我们可以修改animateCircle方法,增加一个完成回调块,这样上一个动画结束后再触发下一个,完美解决顺序问题。

修改后的CircleView代码:

#import "CircleView.h"

@interface CircleView()
@property (nonatomic, strong) CAShapeLayer *circleLayer;
@end

@implementation CircleView

-(instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = UIColor.clearColor;
        UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width / 2., self.frame.size.height / 2.0)
                                                                 radius: (self.frame.size.width - 10) / 2
                                                             startAngle:0.0
                                                               endAngle:M_PI * 2
                                                              clockwise:YES];
        
        CAShapeLayer *downLayer = [[CAShapeLayer alloc] init];
        downLayer.path = circlePath.CGPath;
        downLayer.fillColor = UIColor.clearColor.CGColor;
        downLayer.strokeColor = UIColor.lightGrayColor.CGColor;
        downLayer.lineWidth = 10.0;
        downLayer.strokeEnd = 1.0;
        
        self.circleLayer = [[CAShapeLayer alloc] init];
        self.circleLayer.path = circlePath.CGPath;
        self.circleLayer.fillColor = UIColor.clearColor.CGColor;
        self.circleLayer.strokeColor = UIColor.redColor.CGColor;
        self.circleLayer.lineWidth = 10.0;
        self.circleLayer.strokeEnd = 0.0;
        
        [self.layer addSublayer:downLayer];
        [self.layer addSublayer: self.circleLayer];
    }
    return self;
}

// 新增completion回调参数
-(void)animateCircle:(NSTimeInterval)duration 
            fromPart:(CGFloat)fromPart 
              toPart:(CGFloat)toPart 
           completion:(void(^)(void))completion {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    animation.duration = duration;
    animation.fromValue = @(fromPart);
    animation.toValue = @(toPart);
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    // 保持动画结束后的状态,避免图层跳回初始值
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeForwards;
    
    // 使用CATransaction监听动画完成
    if (completion) {
        [CATransaction begin];
        [CATransaction setCompletionBlock:completion];
    }
    
    [self.circleLayer addAnimation:animation forKey:@"animateCircle"];
    // 更新模型层的值,确保动画结束后状态持久化
    self.circleLayer.strokeEnd = toPart;
    
    if (completion) {
        [CATransaction commit];
    }
}

// 额外优化:如果用AutoLayout,重写这个方法更新环形路径
-(void)layoutSubviews {
    [super layoutSubviews];
    UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width / 2., self.bounds.size.height / 2.0)
                                                             radius: (self.bounds.size.width - 10) / 2
                                                         startAngle:0.0
                                                           endAngle:M_PI * 2
                                                          clockwise:YES];
    self.circleLayer.path = circlePath.CGPath;
    // 别忘了更新底层灰色环的路径
    for (CALayer *layer in self.layer.sublayers) {
        if ([layer isKindOfClass:[CAShapeLayer class]] && layer != self.circleLayer) {
            ((CAShapeLayer *)layer).path = circlePath.CGPath;
        }
    }
}

@end

调用的时候用链式回调:

[self.circleView animateCircle:1 fromPart:0.0 toPart:0.25 completion:^{
    [self.circleView animateCircle:1 fromPart:0.25 toPart:0.5 completion:nil];
}];

方案二:创建动画队列,批量执行

如果有多个进度节点要依次展示,比如下载分多个阶段,把所有动画参数放到队列里,递归执行会更省心:

在CircleView里新增队列相关的属性和方法:

@interface CircleView()
@property (nonatomic, strong) CAShapeLayer *circleLayer;
@property (nonatomic, strong) NSArray *animationQueue;
@property (nonatomic, assign) NSInteger currentAnimIndex;
@end

// 新增队列执行方法
-(void)startAnimationQueue:(NSArray *)animationQueue {
    self.animationQueue = animationQueue;
    self.currentAnimIndex = 0;
    [self executeNextAnimation];
}

-(void)executeNextAnimation {
    if (self.currentAnimIndex >= self.animationQueue.count) {
        return;
    }
    NSDictionary *animInfo = self.animationQueue[self.currentAnimIndex];
    NSTimeInterval duration = [animInfo[@"duration"] doubleValue];
    CGFloat from = [animInfo[@"fromPart"] floatValue];
    CGFloat to = [animInfo[@"toPart"] floatValue];
    
    [self animateCircle:duration fromPart:from toPart:to completion:^{
        self.currentAnimIndex++;
        [self executeNextAnimation];
    }];
}

调用时把所有动画参数打包成数组:

NSArray *progressQueue = @[
    @{@"duration": @1, @"fromPart": @0.0, @"toPart": @0.25},
    @{@"duration": @1, @"fromPart": @0.25, @"toPart": @0.5},
    @{@"duration": @0.5, @"fromPart": @0.5, @"toPart": @0.75} // 可以加更多阶段
];
[self.circleView startAnimationQueue:progressQueue];

这样不管多少个动画阶段,都能按顺序依次执行,完美模拟下载进度的逐步推进~

内容的提问来源于stack exchange,提问作者surToTheW

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 10:08:29