Animated CAShapeLayer Pie

后端 未结 4 2018
遥遥无期
遥遥无期 2020-12-13 16:16

I am trying to create a simple and animated pie chart using CAShapeLayer. I want it to animate from 0 to a provided percentage.

To create the shape laye

相关标签:
4条回答
  • 2020-12-13 16:58

    Quick copy/paste Swift translation of 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)
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-13 17:08

    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()
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-13 17:13

    I know this question has long been answered, but I don't quite think this is a good case for CAShapeLayer and CAKeyframeAnimation. Core Animation has the power to do animation tweening for us. Here's a class (with a wrapping UIView, if you like) that I use to accomplish the effect pretty well.

    The layer subclass enables implicit animation for the progress property, but the view class wraps its setter in a UIView animation method. The interesting (and ultimately useful) side effect of using a 0.0 length animation with UIViewAnimationOptionBeginFromCurrentState is that each animation cancels out the previous one, leading to a smooth, fast, high-framerate pie, like this (animated) and this (not animated, but incremented).

    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 讨论(0)
  • 2020-12-13 17:14

    I suggest you make a keyframe animation instead:

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

    Basic animation is good for scalars/vectors. But do you want it to interpolate your paths?

    0 讨论(0)
提交回复
热议问题