2011-11-19 145 views
7

我試圖創建一個使用CAShapeLayer簡單和動畫餅圖。我希望它從0提供動畫到提供的百分比。顯然動畫CAShapeLayer派

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

,這是一個非常奇怪的方式動畫:

要創建我用的是形狀圖層:

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

然後動畫我用。這種形狀正好成長爲最終狀態。有沒有簡單的方法讓這個動畫跟隨圓的方向?或者這是一個限制CAShapeLayer動畫?

+0

不回答你的問題,但你可能會感興趣的:http://code.google.com/p/core-plot/ –

回答

32

我知道這個問題早已回答,但我不太認爲這是對CAShapeLayerCAKeyframeAnimation一個很好的案例。核心動畫有能力爲我們做補間動畫。這裏有一個課程(如果你喜歡,可以用包裝UIView)來完成這個效果。

層子類使得對進展性隱動畫,但視圖類包裝其設置器在一個UIView動畫方法。使用0.0長度動畫與UIViewAnimationOptionBeginFromCurrentState的有趣(並且最終有用)的副作用是,每個動畫抵消前一個,導致平穩,快速,高幀率餡餅,像this(動畫)和this(未動畫,但遞增)。

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

男人,這段代碼太神奇了。做得好! – marcio

+0

我沒有使動畫工作。你能附上一個示例代碼的鏈接嗎? (GIT hub ..或其他) – abhi

+0

對不起!我已經更新了代碼,和未來的更新可用[礦山的開源代碼塊(https://github.com/zwaldowski/DZProgressController/blob/master/DZProgressController.m)。 – zwaldowski

4

我建議你做一個關鍵幀動畫,而不是:

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

基本的動畫是很好的標量/向量。但是,你想讓它插入你的路徑嗎?

+0

這是一個非常有用的片段,謝謝! –

+0

不錯的解決方案,謝謝 – Slavik

1

快速複製/粘貼的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

在斯威夫特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() 
     } 
    } 
}