2011-11-19 12 views
7

Sto cercando di creare un grafico a torta semplice e animato utilizzando CAShapeLayer. Voglio che si animi da 0 a una percentuale fornita.Animated CAShapeLayer Pie

Per creare il livello di forma che uso:

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

Poi animare io uso:

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"]; 

Ovviamente, questo sta animando in un modo davvero strano. La forma è appena sorta nel suo stato finale. C'è un modo semplice per far sì che questa animazione segua la direzione del cerchio? O è una limitazione delle animazioni CAShapeLayer?

+0

Non è una risposta alla tua domanda, ma si potrebbe essere interessato a questo: http://code.google.com/p/core-plot/ –

risposta

32

So che questa domanda è stata a lungo risposta, ma non credo che questo sia un buon caso per CAShapeLayer e CAKeyframeAnimation. Core Animation ha il potere di fare tweening di animazione per noi. Ecco una classe (con un wrapping UIView, se preferisci) che uso per realizzare l'effetto abbastanza bene.

La sottoclasse livello consente l'animazione implicita per la proprietà di avanzamento, ma la classe di visualizzazione esegue il wrapping del setter in un metodo di animazione UIView. L'effetto collaterale interessante (e in definitiva utile) dell'utilizzo di un'animazione di lunghezza 0,0 con UIViewAnimationOptionBeginFromCurrentState consiste nel fatto che ciascuna animazione annulla la precedente, portando a una torta liscia, veloce e framerata alta, come this (animata) e this (non animata, ma incrementato).

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:(NSString *)key { 
    return [key isEqualToString:@"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:(NSString *) aKey { 
    if ([aKey isEqualToString:@"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 
+0

Man, questo codice è sorprendente. Ottimo lavoro! – marcio

+0

Non riesco a far funzionare l'animazione. potresti allegare un link ad un codice di esempio. (GIT hub..o qualcos'altro) – abhi

+0

Ci scusiamo! Ho aggiornato il codice e sono disponibili aggiornamenti futuri [in un mio codice pezzo open source] (https://github.com/zwaldowski/DZProgressController/blob/master/DZProgressController.m). – zwaldowski

4

vi suggerisco di fare un'animazione keyframe invece:

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

animazione di base è buono per scalari/vettori. Ma vuoi che interpoli i tuoi percorsi?

+0

Questo è stato un frammento molto utile, grazie! –

+0

bella soluzione, grazie – Slavik

1

copia rapida/incolla traduzione Swift di this excellent Objective-C answer.

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: String!) -> Bool{ 

     return key == "progress" || super.needsDisplayForKey(key); 
    } 

    override func actionForKey(event: String!) -> 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) 
     } 
    } 
} 
1

In 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: String) -> Bool{ 

     return key == "progress" || super.needsDisplay(forKey: key); 
    } 

    override func action(forKey event: String) -> 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() 
     } 
    } 
} 
Problemi correlati