Why when I set a low duration value for my CABasic

2019-04-30 22:12发布

Sample Project: http://cl.ly/1W3V3b0D2001

I'm using CABasicAnimation to create a progress indicator that is like a pie chart. Similar to the iOS 7 app download animation:

enter image description here

The animation is set up as follows:

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];

    CGFloat radius = CGRectGetWidth(self.frame) / 2;
    CGFloat inset  = 1;
    CAShapeLayer *ring = [CAShapeLayer layer];
    ring.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
                                           cornerRadius:radius-inset].CGPath;

    ring.fillColor = [UIColor clearColor].CGColor;
    ring.strokeColor = [UIColor whiteColor].CGColor;
    ring.lineWidth = 2;

    self.innerPie = [CAShapeLayer layer];
    inset = radius/2;
    self.innerPie.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
                                               cornerRadius:radius-inset].CGPath;
    self.innerPie.fillColor = [UIColor clearColor].CGColor;
    self.innerPie.strokeColor = [UIColor whiteColor].CGColor;
    self.innerPie.lineWidth = (radius-inset)*2;

    self.innerPie.strokeStart = 0;
    self.innerPie.strokeEnd = 0;

    [self.layer addSublayer:ring];
    [self.layer addSublayer:self.innerPie];

    self.progress = 0.0;
}

The animation is triggered by setting the progress of the view:

- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
    self.progress = progress;

    if (animated) {
        CGFloat totalDurationForFullCircleAnimation = 0.25;

        CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
        self.innerPie.strokeEnd = progress;
        pathAnimation.delegate = self;
        pathAnimation.fromValue = @([self.innerPie.presentationLayer strokeEnd]);
        pathAnimation.toValue = @(progress);
        pathAnimation.duration = totalDurationForFullCircleAnimation * ([pathAnimation.toValue floatValue] - [pathAnimation.fromValue floatValue]);

        [self.innerPie addAnimation:pathAnimation forKey:@"strokeEndAnimation"];
    }
    else {
        [CATransaction setDisableActions:YES];
        [CATransaction begin];
        self.innerPie.strokeEnd = progress;
        [CATransaction commit];
    }
}

However, in cases where I set the progress to something small, such as 0.25, there's a jump in the animation. It goes a little forward clockwise, jumps back, then keeps going forward as normal. It's worth nothing that this does not happen if the duration or progress is set higher.

How do I stop the jump? This code works well in every case except when the progress is very low. What am I doing wrong?

3条回答
男人必须洒脱
2楼-- · 2019-04-30 22:26

When travelling within same distance, the smaller the duration, the higher the speed. When you set the duration too small, the speed will be very high, and that's why it looks like jumping.

查看更多
欢心
3楼-- · 2019-04-30 22:38

Ah! We should have seen this earlier. The problem is this line in your if (animated) block:

self.innerPie.strokeEnd = progress;

Since innerPie is a Core Animation layer, this causes an implicit animation (most property changes do). This animation is fighting with your own animation. You can prevent this from happening by disabling implicit animation while setting the strokeEnd:

[CATransaction begin];
[CATransaction setDisableActions:YES];
self.innerPie.strokeEnd = progress;
[CATransaction commit];

(Note how setDisableActions: is within the begin/commit.)

Alternatively, you can remove your own animation and just use the automatic one, using something like setAnimationDuration: to change its length.

Original Suggestions:

My guess is that your drawRect: is being called, which resets your strokeEnd value. Those layers should probably not be set up in drawRect: anyway. Try moving that setup to an init method or didMoveToWindow: or similar.

If that's not effective, I would suggest adding log statements to track the value of progress and [self.innerPie.presentationLayer strokeEnd] each time the method is called; perhaps they're not doing what you expect.

查看更多
闹够了就滚
4楼-- · 2019-04-30 22:43

Why are you using drawRect:? There should be no drawRect: method in your class.

You should not be using drawRect if you are also using self.layer. Use one or the other, never both.

Change it to something like:

- (id)initWithFrame:(CGRect)frame
{
  if (!(self = [super initWithFrame:frame])
    return nil;

  CGFloat radius = CGRectGetWidth(self.frame) / 2;
  CGFloat inset  = 1;
  CAShapeLayer *ring = [CAShapeLayer layer];
  ring.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
                                        cornerRadius:radius-inset].CGPath;

  ring.fillColor = [UIColor clearColor].CGColor;
  ring.strokeColor = [UIColor whiteColor].CGColor;
  ring.lineWidth = 2;

  self.innerPie = [CAShapeLayer layer];
  inset = radius/2;
  self.innerPie.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
                                            cornerRadius:radius-inset].CGPath;
  self.innerPie.fillColor = [UIColor clearColor].CGColor;
  self.innerPie.strokeColor = [UIColor whiteColor].CGColor;
  self.innerPie.lineWidth = (radius-inset)*2;

  self.innerPie.strokeStart = 0;
  self.innerPie.strokeEnd = 0;

  [self.layer addSublayer:ring];
  [self.layer addSublayer:self.innerPie];

  self.progress = 0.0;

  return self;
}
查看更多
登录 后发表回答