Question

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?

Was it helpful?

Solution

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.

OTHER TIPS

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;
}

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top