Tarte CAShapeLayer animée

J'essaie de créer un graphique à secteurs simple et animé en utilisant CAShapeLayer . Je veux qu'il anime de 0 à un pourcentage fourni.

Pour créer le calque de forme, j'utilise:

 CGMutablePathRef piePath = CGPathCreateMutable(); CGPathMoveToPoint(piePath, NULL, self.frame.size.width/2, self.frame.size.height/2); CGPathAddLineToPoint(piePath, NULL, self.frame.size.width/2, 0); CGPathAddArc(piePath, NULL, self.frame.size.width/2, self.frame.size.height/2, radius, DEGREES_TO_RADIANS(-90), DEGREES_TO_RADIANS(-90), 0); CGPathAddLineToPoint(piePath, NULL, self.frame.size.width/2 + radius * cos(DEGREES_TO_RADIANS(-90)), self.frame.size.height/2 + radius * sin(DEGREES_TO_RADIANS(-90))); pie = [CAShapeLayer layer]; pie.fillColor = [UIColor redColor].CGColor; pie.path = piePath; [self.layer addSublayer:pie]; 

Puis pour animer j'utilise:

 CGMutablePathRef newPiePath = CGPathCreateMutable(); CGPathAddLineToPoint(newPiePath, NULL, self.frame.size.width/2, 0); CGPathMoveToPoint(newPiePath, NULL, self.frame.size.width/2, self.frame.size.height/2); CGPathAddArc(newPiePath, NULL, self.frame.size.width/2, self.frame.size.height/2, radius, DEGREES_TO_RADIANS(-90), DEGREES_TO_RADIANS(125), 0); CGPathAddLineToPoint(newPiePath, NULL, self.frame.size.width/2 + radius * cos(DEGREES_TO_RADIANS(125)), self.frame.size.height/2 + radius * sin(DEGREES_TO_RADIANS(125))); CABasicAnimation *pieAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; pieAnimation.duration = 1.0; pieAnimation.removedOnCompletion = NO; pieAnimation.fillMode = kCAFillModeForwards; pieAnimation.fromValue = pie.path; pieAnimation.toValue = newPiePath; [pie addAnimation:pieAnimation forKey:@"animatePath"]; 

Évidemment, cela s'anime d'une manière vraiment étrange. La forme se développe en quelque sorte dans son état final. Existe-t-il un moyen facile de faire en sorte que cette animation suive la direction du cercle? Ou est-ce une limitation des animations CAShapeLayer ?

Je sais que cette question a longtime été répondue, mais je ne pense pas que ce soit un bon cas pour CAShapeLayer et CAKeyframeAnimation . Core Animation a le pouvoir de faire du tweening d'animation pour nous. Voici une class (avec un UIView enveloppant, si vous voulez) que j'utilise pour accomplir l'effet plutôt bien.

La sous-class layer permet une animation implicite pour la propriété progress, mais la class view enveloppe son setter dans une méthode d'animation UIView . L'effet secondaire intéressant (et finalement utile) de l'utilisation d'une animation de 0.0 avec UIViewAnimationOptionBeginFromCurrentState est que chaque animation annule la précédente, conduisant à une tarte lisse, rapide, à haut débit, comme ceci (animé) et ceci (non animé, mais incrémenté).

DZRoundProgressView.h

 @interface DZRoundProgressLayer : CALayer @property (nonatomic) CGFloat progress; @end @interface DZRoundProgressView : UIView @property (nonatomic) CGFloat progress; @end 

DZRoundProgressView.m

 #import "DZRoundProgressView.h" #import <QuartzCore/QuartzCore.h> @implementation DZRoundProgressLayer // Using Core Animation's generated properties allows // it to do tweening for us. @dynamic progress; // This is the core of what does animation for us. It // tells CoreAnimation that it needs to redisplay on // each new value of progress, including tweened ones. + (BOOL)needsDisplayForKey:(NSSsortingng *)key { return [key isEqualToSsortingng:@"progress"] || [super needsDisplayForKey:key]; } // This is the other crucial half to tweening. // The animation we return is compatible with that // used by UIView, but it also enables implicit // filling-up-the-pie animations. - (id)actionForKey:(NSSsortingng *) aKey { if ([aKey isEqualToSsortingng:@"progress"]) { CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:aKey]; animation.fromValue = [self.presentationLayer valueForKey:aKey]; return animation; } return [super actionForKey:aKey]; } // This is the gold; the drawing of the pie itself. // In this code, it draws in a "HUD"-y style, using // the same color to fill as the border. - (void)drawInContext:(CGContextRef)context { CGRect circleRect = CGRectInset(self.bounds, 1, 1); CGColorRef borderColor = [[UIColor whiteColor] CGColor]; CGColorRef backgroundColor = [[UIColor colorWithWhite: 1.0 alpha: 0.15] CGColor]; CGContextSetFillColorWithColor(context, backgroundColor); CGContextSetStrokeColorWithColor(context, borderColor); CGContextSetLineWidth(context, 2.0f); CGContextFillEllipseInRect(context, circleRect); CGContextStrokeEllipseInRect(context, circleRect); CGFloat radius = MIN(CGRectGetMidX(circleRect), CGRectGetMidY(circleRect)); CGPoint center = CGPointMake(radius, CGRectGetMidY(circleRect)); CGFloat startAngle = -M_PI / 2; CGFloat endAngle = self.progress * 2 * M_PI + startAngle; CGContextSetFillColorWithColor(context, borderColor); CGContextMoveToPoint(context, center.x, center.y); CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0); CGContextClosePath(context); CGContextFillPath(context); [super drawInContext:context]; } @end @implementation DZRoundProgressView + (Class)layerClass { return [DZRoundProgressLayer class]; } - (id)init { return [self initWithFrame:CGRectMake(0.0f, 0.0f, 37.0f, 37.0f)]; } - (id)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { self.opaque = NO; self.layer.contentsScale = [[UIScreen mainScreen] scale]; [self.layer setNeedsDisplay]; } return self; } - (void)setProgress:(CGFloat)progress { [(id)self.layer setProgress:progress]; } - (CGFloat)progress { return [(id)self.layer progress]; } @end 

Je vous suggère de faire une animation par image key à la place:

 pie.bounds = CGRectMake(-0.5 * radius, -0.5 * radius, radius, radius); NSMutableArray *values = [NSMutableArray array]; for (int i = 0; i < nrSteps + 1; i++) { UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointZero]; [path addLineToPoint:CGPointMake(radius * cosf(startAngle), radius * sinf(startAngle))]; [path addArcWithCenter:... endAngle:startAngle + i * (endAngle - startAngle) / nrSteps ...]; [path closePath]; [values addObject:(__bridge id)path.CGPath]; } 

L'animation de base est bonne pour les scalaires / vectors. Mais voulez-vous interpoler vos paths?

Copie rapide / coller Traduction rapide de cette excellente réponse Objective-C .

 class ProgressView: UIView { required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) genericInit() } private func genericInit() { self.opaque = false; self.layer.contentsScale = UIScreen.mainScreen().scale self.layer.setNeedsDisplay() } var progress : CGFloat = 0 { didSet { (self.layer as! ProgressLayer).progress = progress } } override class func layerClass() -> AnyClass { return ProgressLayer.self } func updateWith(progress : CGFloat) { self.progress = progress } } class ProgressLayer: CALayer { @NSManaged var progress : CGFloat override class func needsDisplayForKey(key: Ssortingng!) -> Bool{ return key == "progress" || super.needsDisplayForKey(key); } override func actionForKey(event: Ssortingng!) -> CAAction! { if event == "progress" { let animation = CABasicAnimation(keyPath: event) animation.duration = 0.2 animation.fromValue = self.presentationLayer().valueForKey(event) return animation } return super.actionForKey(event) } override func drawInContext(ctx: CGContext!) { if progress != 0 { let circleRect = CGRectInset(self.bounds, 1, 1) let borderColor = UIColor.whiteColor().CGColor let backgroundColor = UIColor.clearColor().CGColor CGContextSetFillColorWithColor(ctx, backgroundColor) CGContextSetStrokeColorWithColor(ctx, borderColor) CGContextSetLineWidth(ctx, 2) CGContextFillEllipseInRect(ctx, circleRect) CGContextStrokeEllipseInRect(ctx, circleRect) let radius = min(CGRectGetMidX(circleRect), CGRectGetMidY(circleRect)) let center = CGPointMake(radius, CGRectGetMidY(circleRect)) let startAngle = CGFloat(-(M_PI/2)) let endAngle = CGFloat(startAngle + 2 * CGFloat(M_PI * Double(progress))) CGContextSetFillColorWithColor(ctx, borderColor) CGContextMoveToPoint(ctx, center.x, center.y) CGContextAddArc(ctx, center.x, center.y, radius, startAngle, endAngle, 0) CGContextClosePath(ctx) CGContextFillPath(ctx) } } } 

Dans Swift 3

 class ProgressView: UIView { required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder)! genericInit() } private func genericInit() { self.isOpaque = false; self.layer.contentsScale = UIScreen.main.scale self.layer.setNeedsDisplay() } var progress : CGFloat = 0 { didSet { (self.layer as! ProgressLayer).progress = progress } } override class var layerClass: AnyClass { return ProgressLayer.self } func updateWith(progress : CGFloat) { self.progress = progress } } class ProgressLayer: CALayer { @NSManaged var progress : CGFloat override class func needsDisplay(forKey key: Ssortingng) -> Bool{ return key == "progress" || super.needsDisplay(forKey: key); } override func action(forKey event: Ssortingng) -> CAAction? { if event == "progress" { let animation = CABasicAnimation(keyPath: event) animation.duration = 0.2 animation.fromValue = self.presentation()?.value(forKey: event) ?? 0 return animation } return super.action(forKey: event) } override func draw(in ctx: CGContext) { if progress != 0 { let circleRect = self.bounds.insetBy(dx: 1, dy: 1) let borderColor = UIColor.white.cgColor let backgroundColor = UIColor.red.cgColor ctx.setFillColor(backgroundColor) ctx.setStrokeColor(borderColor) ctx.setLineWidth(2) ctx.fillEllipse(in: circleRect) ctx.strokeEllipse(in: circleRect) let radius = min(circleRect.midX, circleRect.midY) let center = CGPoint(x: radius, y: circleRect.midY) let startAngle = CGFloat(-(Double.pi/2)) let endAngle = CGFloat(startAngle + 2 * CGFloat(Double.pi * Double(progress))) ctx.setFillColor(borderColor) ctx.move(to: CGPoint(x:center.x , y: center.y)) ctx.addArc(center: CGPoint(x:center.x, y: center.y), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) ctx.closePath() ctx.fillPath() } } }