我正在尝试使用CAShapeLayer
创建一个简单的动画饼图。我想让它从0动画到提供的百分比。
要创建形状图层,我使用:
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];
然后动画我使用:
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"];
显然,这是以一种非常奇怪的方式制作动画。形状只是变成了最终状态。有没有一种简单的方法可以让这个动画跟随圆圈的方向?或者这是CAShapeLayer
动画的限制吗?
答案 0 :(得分:34)
我知道这个问题早已得到解答,但我不认为这是CAShapeLayer
和CAKeyframeAnimation
的好例子。 Core Animation有能力为我们做动画补间动画。这是一个类(包含UIView
,如果你愿意的话),我用它来很好地完成效果。
图层子类为progress属性启用隐式动画,但视图类将其setter包装在UIView
动画方法中。使用带有UIViewAnimationOptionBeginFromCurrentState
的0.0长度动画的有趣(并且最终有用)副作用是每个动画取消前一个动画,从而产生平滑,快速,高帧率的饼图,如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
答案 1 :(得分: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];
}
基本动画适用于标量/矢量。但是你想让它插入你的路径吗?
答案 2 :(得分:2)
快速复制/粘贴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)
}
}
}
答案 3 :(得分:1)
在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()
}
}
}