Search code examples
iosanimationcore-graphicsduration

CoreGraphics Circle Animation Wrong Duration


Edit: I think the best is to convert the code to CoreAnimation. How would this code be converted to that method? I believe the issue is the fact that there is a difference in device FPS.

I have been searching for a custom progress circle for iOS and come across this library: https://github.com/Eclair/CircleProgressBar

I have noticed however that there is a bug whenever animating for about over 5 seconds. The animation of the circle is slower than it should be. For example: I animate the circle to 100% completed in 30 seconds however it only reaches like 78% in that time period (the "Done" label is visible after a NSTimer of 30 seconds). Check out this image I took in a demo project I made: enter image description here

This is the code to make the circle:

- (void)drawProgressBar:(CGContextRef)context progressAngle:(CGFloat)progressAngle center:(CGPoint)center radius:(CGFloat)radius {
    CGFloat barWidth = self.progressBarWidthForDrawing;
    if (barWidth > radius) {
        barWidth = radius;
    }

    CGContextSetFillColorWithColor(context, self.progressBarProgressColorForDrawing.CGColor);
    CGContextBeginPath(context);
    CGContextAddArc(context, center.x, center.y, radius, DEGREES_TO_RADIANS(_startAngle), DEGREES_TO_RADIANS(progressAngle), 0);
    CGContextAddArc(context, center.x, center.y, radius - barWidth, DEGREES_TO_RADIANS(progressAngle), DEGREES_TO_RADIANS(_startAngle), 1);
    CGContextClosePath(context);
    CGContextFillPath(context);

    CGContextSetFillColorWithColor(context, self.progressBarTrackColorForDrawing.CGColor);
    CGContextBeginPath(context);
    CGContextAddArc(context, center.x, center.y, radius, DEGREES_TO_RADIANS(progressAngle), DEGREES_TO_RADIANS(_startAngle + 360), 0);
    CGContextAddArc(context, center.x, center.y, radius - barWidth, DEGREES_TO_RADIANS(_startAngle + 360), DEGREES_TO_RADIANS(progressAngle), 1);
    CGContextClosePath(context);
    CGContextFillPath(context);
}

This is the code that animates the circle:

- (void)animateProgressBarChangeFrom:(CGFloat)startProgress to:(CGFloat)endProgress duration:(CGFloat)duration {
    _currentAnimationProgress = _startProgress = startProgress;
    _endProgress = endProgress;

    _animationProgressStep = (_endProgress - _startProgress) * AnimationChangeTimeStep / duration;

    _animationTimer = [NSTimer scheduledTimerWithTimeInterval:AnimationChangeTimeStep target:self selector:@selector(updateProgressBarForAnimation) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_animationTimer forMode:NSRunLoopCommonModes];
}

- (void)updateProgressBarForAnimation {
    _currentAnimationProgress += _animationProgressStep;
    _progress = _currentAnimationProgress;
    if ((_animationProgressStep > 0 && _currentAnimationProgress >= _endProgress) || (_animationProgressStep < 0 && _currentAnimationProgress <= _endProgress)) {
        [_animationTimer invalidate];
        _animationTimer = nil;
        _progress = _endProgress;
    }
    [self setNeedsDisplay];
}

Is there anything here that looks wrong? Is there a better way to animate the circle? I would ideally want it to animate with the correct duration and I am just not that familiar in how to fix this.

Demo Project to showcase issue: https://ufile.io/8pkt5


Solution

  • Basically drawRect method works on main thread and since you're using too small AnimationChangeTimeStep:

    const CGFloat AnimationChangeTimeStep = 0.01f;
    

    It's 100 frames per second, which is too much and drawRect blocks main thread and animation timer attached to that thread, so repeats of animation timer fires not immediately but after all current drawRect calls finished. So to fix your problem just decrease AnimationChangeTimeStep and make it equal to frame rate 30 fps:

    const CGFloat AnimationChangeTimeStep = 1/30.0f;