CAShapeLayer 渲染问题

CAShapeLayer rendering issue

我正在尝试通过添加具有不同描边颜色的 CAShapeLayer 来创建圆形动画:

    UIView *view = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
    [self.view addSubview:view];
    view.backgroundColor = [UIColor redColor];
    [self addCircleAnimationTo:view direction:YES];

- (void)addCircleAnimationTo:(UIView *)view direction:(BOOL)direction {
    CGFloat animationTime = 3;

    // Set up the shape of the circle
    int radius = view.frame.size.width/2;
    CAShapeLayer *circle = [CAShapeLayer layer];

    // Make a circular shape
    circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
                                             cornerRadius:radius].CGPath;

    // Center the shape in view
    circle.position = CGPointMake(radius,
                                  radius);

    circle.bounds = view.bounds;
    // Configure the apperence of the circle
    circle.fillColor = [UIColor clearColor].CGColor;

    // switch color
    circle.strokeColor = (direction) ? [UIColor blueColor].CGColor : [UIColor whiteColor].CGColor;
    circle.lineWidth = 5;
    circle.lineJoin = kCALineJoinBevel;
    circle.strokeStart = .0;
    circle.strokeEnd = 1.;

    // Configure animation
    CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    drawAnimation.duration            = animationTime;
    drawAnimation.repeatCount         = 1;

    // Animate from no part of the stroke being drawn to the entire stroke being drawn
    drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
    drawAnimation.toValue   = [NSNumber numberWithFloat:1.];

    // Experiment with timing to get the appearence to look the way you want
    drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];

    // Add to parent layer
    [view.layer addSublayer:circle];
    // Add the animation to the circle
    [circle addAnimation:drawAnimation forKey:@"drawCircleAnimation"];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((animationTime-0.1) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self addCircleAnimationTo:view direction:!direction];
    });
}

我想知道为什么当白色图层动画时我可以看到蓝色以及如何摆脱这个奇怪的蓝色边框:

抗锯齿是导致问题的原因

当白色 CAShapeLayer 绘制在蓝色之上时,边缘会与抗锯齿创建的先前边缘混合混合。

唯一真正的解决方案是在设置动画时清除白色下方的蓝色。您可以通过设置 strokeStart 属性 的动画来实现前一层从 0.01.0。为此,您应该能够 re-use 您的 drawAnimation,只需更改 keyPath。例如,将 dispatch_after 更改为:

- (void)addCircleAnimationTo:(UIView *)view direction:(BOOL)direction {

    // Insert your previous animation code, where drawAnimation is defined and added to the circle layer.    


    drawAnimation.keyPath = @"strokeStart"; // Re-use the draw animation object, setting it to animate strokeStart now.

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((animationTime-0.1) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self addCircleAnimationTo:view direction:!direction];
        [circle addAnimation:drawAnimation forKey:@"(un)drawCircleAnimation"]; // Sets the previous layer to animate out
    });

}

这样,蓝色层的抗锯齿边缘混合就会被清除,从而使白色层获得漂亮干净的抗锯齿效果。 动画完成后别忘了删除图层!

另一种可能的解决方案是增加白色图层的描边宽度,使其描边超过蓝色图层的边缘混合,但我想这会产生不良效果。您也可以禁用抗锯齿,但这在圆圈上看起来很糟糕。

你知道每次调用 addCircleAnimationTo 后你都会添加越来越多的形状图层吗?

当您为新图层设置动画时,旧形状图层会在新图层下方提醒。这就是你看到它的原因。

[编辑 2] 简短说明

我们创建两个层,第一个是蓝色路径颜色,第二个是白色路径颜色。

我们在第一层添加 fill 动画 - layer.strokeEnd 属性 的变化,我们将其设置为从 0 到 1 和从 1 到 1 的动画。一半从 0 到 1 的持续时间和从 1 到 1 的一半持续时间(视觉上没有任何反应,但是需要)。

我们添加清除动画-layer.strokeStart的变化,我们将它从0动画到1。这个动画的持续时间是持续时间的一半填充 动画。开始时间也是 fill 动画的一半,因为我们想在结束笔画静止时移动笔画的开始。

我们将相同的动画添加到第二个白色层,但具有适当的开始时间偏移,这要归功于白色路径展开和蓝色路径缠绕。

[编辑] 添加真实答案

这是我解决动画问题的建议。希望对您有所帮助...:)

- (void)viewDidLoad {
    [super viewDidLoad];

    UIView *view = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
    [self.view addSubview:view];
    view.backgroundColor = [UIColor redColor];

    self.circle1 = [self circle:view.bounds];
    self.circle1.strokeColor = [UIColor blueColor].CGColor;
    self.circle2 = [self circle:view.bounds];
    self.circle2.strokeColor = [UIColor whiteColor].CGColor;

    [view.layer addSublayer:self.circle1];
    [view.layer addSublayer:self.circle2];

    [self.circle1 addAnimation:[self fillClearAnimation:0] forKey:@"fillclear"];
    [self.circle2 addAnimation:[self fillClearAnimation:3] forKey:@"fillclear"];
}

- (CAAnimationGroup *)fillClearAnimation:(CFTimeInterval)offset
{
    CGFloat animationTime = 3;

    CAAnimationGroup *group = [CAAnimationGroup animation];

    CAKeyframeAnimation *fill = [CAKeyframeAnimation animationWithKeyPath:@"strokeEnd"];
    fill.values = @[ @0,@1,@1];
    fill.duration = 2*animationTime;
    fill.beginTime = 0.f;
    CAKeyframeAnimation *clear = [CAKeyframeAnimation animationWithKeyPath:@"strokeStart"];
    clear.values = @[ @0, @1];
    clear.beginTime = animationTime;
    clear.duration = animationTime;

    group.animations = @[ fill, clear ];
    group.duration = 2*animationTime;
    group.beginTime = CACurrentMediaTime() + offset;

    group.repeatCount = FLT_MAX;

    return group;
}

- (CAShapeLayer *)circle:(CGRect)frame
{
    CAShapeLayer *circle = [CAShapeLayer layer];
    CGFloat radius = ceil(frame.size.width/2);

    // Make a circular shape
    circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
                                             cornerRadius:radius].CGPath;

    // Center the shape in view
    circle.position = CGPointMake(radius,
                                  radius);

    circle.frame = frame;
    // Configure the apperence of the circle
    circle.fillColor = [UIColor clearColor].CGColor;

    circle.lineWidth = 5;
    circle.lineJoin = kCALineJoinBevel;
    circle.strokeStart = 0.f;
    circle.strokeEnd = 0.f;

    return circle;
}