2011-11-19 15 views
7

Tôi đang cố gắng tạo biểu đồ hình tròn đơn giản và hoạt hình bằng cách sử dụng CAShapeLayer. Tôi muốn nó hoạt hình từ 0 đến tỷ lệ phần trăm được cung cấp.Animated CAShapeLayer Pie

Để tạo shape layer Tôi sử dụng:

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

Sau đó để animate tôi sử dụng:

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

Rõ ràng, đây là hiệu ứng động một cách thực sự kỳ lạ. Hình dạng chỉ phát triển thành trạng thái cuối cùng của nó. Có cách nào dễ dàng để tạo hoạt ảnh này theo hướng của vòng tròn không? Hoặc đó là giới hạn của CAShapeLayer hoạt ảnh?

+0

Không phải là câu trả lời cho câu hỏi của bạn, nhưng bạn có thể quan tâm này: http://code.google.com/p/core-plot/ –

Trả lời

32

Tôi biết câu hỏi này từ lâu đã được trả lời, nhưng tôi không nghĩ rằng đây là một trường hợp tốt cho CAShapeLayerCAKeyframeAnimation. Core Animation có sức mạnh để làm hoạt hình tweening cho chúng tôi. Đây là một lớp (với một gói UIView, nếu bạn thích) mà tôi sử dụng để thực hiện hiệu quả khá tốt.

Lớp con lớp cho phép hoạt ảnh ngầm cho thuộc tính tiến trình, nhưng lớp xem kết thúc tốt hơn bộ đặt của nó theo phương thức hoạt ảnh UIView. Hiệu ứng phụ thú vị (và cuối cùng hữu ích) của việc sử dụng hoạt ảnh có độ dài 0.0 với UIViewAnimationOptionBeginFromCurrentState là mỗi hoạt ảnh hủy bỏ ảnh trước đó, dẫn đến một chiếc bánh mịn, nhanh, có tốc độ cao, như this (hoạt ảnh) và this (không hoạt ảnh, nhưng tăng dần).

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

Con người, mã này thật tuyệt vời. Làm tốt lắm! – marcio

+0

Tôi không nhận được hoạt ảnh hoạt động. bạn có thể đính kèm một liên kết đến một mã mẫu không. (GIT hub..or something else) – abhi

+0

Xin lỗi về điều đó! Tôi đã cập nhật mã và các bản cập nhật trong tương lai có sẵn [trong mã nguồn mở của tôi] (https://github.com/zwaldowski/DZProgressController/blob/master/DZProgressController.m). – zwaldowski

4

Tôi đề nghị bạn thực hiện một phim hoạt hình keyframe thay vì:

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

hoạt hình cơ bản là tốt cho vô hướng/vectơ. Nhưng bạn có muốn nó để nội suy đường dẫn của bạn?

+0

Đó là một đoạn trích rất hữu ích, cảm ơn bạn! –

+0

giải pháp tốt đẹp, cảm ơn – Slavik

1

copy nhanh/dán dịch Swift của 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

Trong 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() 
     } 
    } 
}