Core Animation Progress Callback

Core animation progress callback

I've finally developed a solution for this problem.

Essentially I wish to be called back for every frame and do what I need to do.

There's no obvious way to observe the progress of an animation, however it is actually possible:

  • Firstly we need to create a new subclass of CALayer that has an animatable property called 'progress'.

  • We add the layer into our tree, and then create an animation that will drive the progress value from 0 to 1 over the duration of the animation.

  • Since our progress property can be animated, drawInContext is called on our sublass for every frame of an animation. This function doesn't need to redraw anything, however it can be used to call a delegate function :)

Here's the class interface:

@protocol TAProgressLayerProtocol 

- (void)progressUpdatedTo:(CGFloat)progress;

@end

@interface TAProgressLayer : CALayer

@property CGFloat progress;
@property (weak) id delegate;

@end

And the implementation:

@implementation TAProgressLayer

// We must copy across our custom properties since Core Animation makes a copy
// of the layer that it's animating.

- (id)initWithLayer:(id)layer
{
self = [super initWithLayer:layer];
if (self) {
TAProgressLayer *otherLayer = (TAProgressLayer *)layer;
self.progress = otherLayer.progress;
self.delegate = otherLayer.delegate;
}
return self;
}

// Override needsDisplayForKey so that we can define progress as being animatable.

+ (BOOL)needsDisplayForKey:(NSString*)key {
if ([key isEqualToString:@"progress"]) {
return YES;
} else {
return [super needsDisplayForKey:key];
}
}

// Call our callback

- (void)drawInContext:(CGContextRef)ctx
{
if (self.delegate)
{
[self.delegate progressUpdatedTo:self.progress];
}
}

@end

We can then add the layer to our main layer:

TAProgressLayer *progressLayer = [TAProgressLayer layer];
progressLayer.frame = CGRectMake(0, -1, 1, 1);
progressLayer.delegate = self;
[_sceneView.layer addSublayer:progressLayer];

And animate it along with the other animations:

CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"progress"];
anim.duration = 4.0;
anim.beginTime = 0;
anim.fromValue = @0;
anim.toValue = @1;
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;

[progressLayer addAnimation:anim forKey:@"progress"];

Finally, the delegate will be called back as the animation progresses:

- (void)progressUpdatedTo:(CGFloat)progress
{
// Do whatever you need to do...
}

CABasicAnimation progress with variable speed

Thanks to @danh for pointing me to this answer Core animation progress callback.

It helped me.

I added the subclassed CALayer, animated it the same way I animated my layer (with the same speed variations). That way, I can get the progress of the main animation.

Animation End Callback for CALayer?

I answered my own question. You have to add an animation using CABasicAnimation like so:

CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:@"frame"];
anim.fromValue = [NSValue valueWithCGRect:layer.frame];
anim.toValue = [NSValue valueWithCGRect:frame];
anim.delegate = self;
[layer addAnimation:anim forKey:@"frame"];

And implement the delegate method animationDidStop:finished: and you should be good to go. Thank goodness this functionality exists! :D

How to make an animate progress bar with startup animation using css

Have a look at CSS position property, you need position relative and position absolute.

Then the animation-fill-mode

forwards
The target will retain the computed values set by the last keyframe encountered during execution. The last keyframe depends on the value of animation-direction and animation-iteration-count:

@keyframes slideInFromLeft {  to{ width: 50%; }}
.progress { width:200px; height:50px; border:1px solid black; position:relative;}
.child { position:absolute; left:0; top:0; background:black; width: 0%; height: 100%; overflow:hidden; animation: slideInFromLeft 1s ease forwards;}
x

How to animate custom Progress bar properly (swift)?

After digging a while and a ton of testing, i came up with a solution, that suited my needs! Altough the above answer from DonMag was also working great (thanks for your effort), i wanted to fix what halfway worked. So the problem was, that the bar resized itself from the middle of the view. And on top, the position was also off for some reason.

First i set the position back to (0,0) so that the view started at the beginning (where it should).
The next thing was the resizing from the middle, because with the position set back, the bar only animated to the half when i set it to 100%. After some tinkering and reading i found out, that changing the anchorPoint of the view would solve my problem. The default value was (0.5,0.5), changing it into (0,0) meant that it would only expand the desired direction.

After that i only needed to re-set the end of the gradient, so that the animation stays consistent between the different values. After all of this my bar worked like I imagined. And here is the result:

Sample Image

Here is the final code, i used to accomplish this:

func setProgress(to percent : CGFloat)
{
progress = percent
print(percent)
let duration = 0.5

let rect = self.bounds
let oldBounds = progressLayer.bounds
let newBounds = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))


let redrawAnimation = CABasicAnimation(keyPath: "bounds")
redrawAnimation.fromValue = oldBounds
redrawAnimation.toValue = newBounds

redrawAnimation.fillMode = .both
redrawAnimation.isRemovedOnCompletion = false
redrawAnimation.duration = duration

progressLayer.bounds = newBounds
progressLayer.position = CGPoint(x: 0, y: 0)

progressLayer.anchorPoint = CGPoint(x: 0, y: 0)

progressLayer.add(redrawAnimation, forKey: "redrawAnim")

let oldGradEnd = gradientLayer.endPoint
let newGradEnd = CGPoint(x: progress, y: 0.5)
let gradientEndAnimation = CABasicAnimation(keyPath: "endPoint")
gradientEndAnimation.fromValue = oldGradEnd
gradientEndAnimation.toValue = newGradEnd

gradientEndAnimation.fillMode = .both
gradientEndAnimation.isRemovedOnCompletion = false
gradientEndAnimation.duration = duration

gradientLayer.endPoint = newGradEnd

gradientLayer.add(gradientEndAnimation, forKey: "gradEndAnim")

}

What's the meaning of progress keypath in CABasicAnimation?

From this answer (which is coincidentally animating progress too):

Firstly we need to create a new subclass of CALayer that has an animatable property called 'progress'.

Your snippet does indeed have a property called 'progress'

@property(nonatomic) CGFloat progress;

It appears the animation is animating that property

How to show a static NON-animated progress bar using CoreGraphics in Swift?

Here you have a workin, designable class

import Foundation
import UIKit

@IBDesignable
class StaticLoad: UIView {

enum Progress {
case Empty
case Filled(num: Int)
case Full
}

@IBInspectable public var progressColor: UIColor? {
didSet {
updateColor(progressColor: progressColor ?? tintColor, nonProgressColor: nonProgressColor)
}
}

@IBInspectable public var nonProgressColor: UIColor = .gray {
didSet {
updateColor(progressColor: progressColor ?? tintColor, nonProgressColor: nonProgressColor)
}
}

@IBInspectable public var numberOfSteps: Int = 3 {
didSet {
redrawSteps(step: numberOfSteps)
updateColor(progressColor: progressColor ?? self.tintColor, nonProgressColor: nonProgressColor)
}
}

@IBInspectable public var filledProgess: Int = 1 {
didSet {
if filledProgess >= numberOfSteps {
filledProgess = numberOfSteps
progress = .Full
}
if filledProgess <= 0 {
filledProgess = 0
progress = .Empty
} else {
progress = .Filled(num: filledProgess)
}
updateColor(progressColor: progressColor ?? tintColor, nonProgressColor: nonProgressColor)
}
}

private var tip: CGFloat = 5
private var progress: Progress = .Empty
private var progressLayers: [CAShapeLayer] = []

private func updateColor(progressColor: UIColor, nonProgressColor: UIColor) {
switch progress {
case .Empty:
progressLayers.forEach { $0.fillColor = nonProgressColor.cgColor }
case let .Filled(num):
progressLayers.dropFirst(num).forEach { $0.fillColor = nonProgressColor.cgColor }
progressLayers.dropLast(progressLayers.count - num).forEach { $0.fillColor = progressColor.cgColor }
case .Full :
progressLayers.forEach { $0.fillColor = progressColor.cgColor }
}
}

private func redrawSteps(step: Int) {
progressLayers.forEach { $0.removeFromSuperlayer() }
progressLayers.removeAll()
if step == 0 {
let layer = CAShapeLayer()
layer.frame = self.bounds
progressLayers.append(layer)
} else {
var topLeft = CGPoint(x: 0, y: 0)
let cgStep = CGFloat(step)
let avaibleWidth = self.bounds.width - (2 * (cgStep - 1))
let spacePerStep = avaibleWidth / cgStep
let height = bounds.height
let halfHeight = height / 2
for i in 0..
let layer = CAShapeLayer()
let bezier = UIBezierPath()
bezier.move(to: topLeft)
bezier.addLine(to: CGPoint(x: topLeft.x + spacePerStep, y: topLeft.y))
if i + 1 != step {
bezier.addLine(to: CGPoint(x: topLeft.x + spacePerStep + tip, y: topLeft.y + halfHeight))
}
bezier.addLine(to: CGPoint(x: topLeft.x + spacePerStep, y: topLeft.y + height))
bezier.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y + height))
if i != 0 {
bezier.addLine(to: CGPoint(x: topLeft.x + tip, y: topLeft.y + halfHeight))
}
bezier.close()
layer.path = bezier.cgPath
progressLayers.append(layer)
self.layer.addSublayer(layer)

topLeft.x = topLeft.x + spacePerStep + 2
}
}
}

override func layoutSubviews() {
redrawSteps(step: numberOfSteps)
updateColor(progressColor: progressColor ?? tintColor, nonProgressColor: nonProgressColor)
}

}

All you have to do is take a UIView from the storyboard and make the custom class StaticLoad. You can specify the number of bars in the progress bar by changing the numberOfSteps property either by code or by the storyboard, and the fill of the bar by filledProgress

If you want a bigger arrow, just change the tip property, or specify another path for the bezierPath.

If you don't know, the @IBDesignable (it is recommended that if a class is designable it should be put in another framework rather than the same project due to the continuous compilation) allows the storyboard to render the view and the @IBInspectable allow to set the variable through the storyboard.

This is a working not-optimized example.



Related Topics



Leave a reply



Submit