CALayer大部分屬性都可以添加CAAnimation動畫,動畫添加到layer上之后就會自動開始執(zhí)行,但這僅限于CALayer及其子類已有的屬性,如果是自己添加的屬性,是不會自動產(chǎn)生動畫的,如果需要動畫效果,就需要自定義動畫了。
如何創(chuàng)建自定義動畫?
大體來說有兩種方法,一種是使用系統(tǒng)的繪制方法畫出動畫,一種是利用定時器自己繪制動畫。
方法一
1、創(chuàng)建自定義Layer類繼承自CAShapeLayer
2、給Layer添加需要執(zhí)行動畫的屬性,在.m文件中使用@dynamic聲明動畫屬性
3、重寫幾個系統(tǒng)方法:
// 告訴CALayer自定義屬性需要進行重繪
+ (BOOL)needsDisplayForKey:(NSString *)key {
if ([key isEqualToString:@"startAngle"]||[key isEqualToString:@"endAngle"]) {
return YES;
}
return [super needsDisplayForKey:key];
}
// 如果自定義屬性值改變,就生成動畫,系統(tǒng)會自動調(diào)用display方法重繪
- (id<CAAction>)actionForKey:(NSString *)event {
if ([event isEqualToString:@"startAngle"])
{
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:event];
anim.duration = 1;
anim.fromValue = @([self.presentationLayer startAngle]);
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
return anim;
} else if ([event isEqualToString:@"endAngle"]) {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:event];
anim.duration = 1;
anim.fromValue = @([self.presentationLayer endAngle]);
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
return anim;
}
return [super actionForKey:event];
}
重寫上面兩個方法之后,在外部修改動畫屬性時就會觸發(fā)界面重繪。系統(tǒng)會優(yōu)先調(diào)用-display
方法進行重繪,如果沒有重寫這個方法,系統(tǒng)會調(diào)用-drawInContext:
進行重繪。要注意:
不在-drawInContext中繪圖的時候,要在繪圖代碼前后加上:
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
在-drawInContext中繪制的時候不需要寫。
繪制代碼:
- (void)drawInContext:(CGContextRef)ctx {
CGPoint center = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
CGFloat presentStartAngle = [[self.presentationLayer valueForKey:@"startAngle"] doubleValue];
CGFloat presentEndAngle = [[self.presentationLayer valueForKey:@"endAngle"] doubleValue];
CGContextSetLineWidth(ctx, 40);
CGContextSetStrokeColorWithColor(ctx, [UIColor purpleColor].CGColor);
CGContextAddArc(ctx, center.x, center.y, 100, presentStartAngle, presentEndAngle, 0);
CGContextStrokePath(ctx);
}
- (void)display {
CGPoint center = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
CGFloat presentStartAngle = [[self.presentationLayer valueForKey:@"startAngle"] doubleValue];
CGFloat presentEndAngle = [[self.presentationLayer valueForKey:@"endAngle"] doubleValue];
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(ctx, 40);
[self.color setStroke];
CGContextAddArc(ctx, center.x, center.y, 100, presentStartAngle, presentEndAngle, 0);
CGContextStrokePath(ctx);
self.contents = (id)UIGraphicsGetImageFromCurrentImageContext().CGImage;
UIGraphicsEndImageContext();
}
創(chuàng)建這個Layer對象的時候,需要給它設(shè)置frame,如果不設(shè)置frame,會出現(xiàn)繪制失敗。所以最好用-initWithFrame:
方法進行創(chuàng)建。
方法二
理論上講重繪方法每秒會被調(diào)用60次,但真機實測發(fā)現(xiàn)只有50多次,并且調(diào)用次數(shù)會隨著重繪代碼的增多而減少,這會造成動畫幀數(shù)下降,使動畫看起來不夠流暢。
第二種方法是創(chuàng)建定時器,每秒觸發(fā)60次,每次觸發(fā)都進行重繪,步驟:
1、創(chuàng)建Layer類繼承自CAShapeLayer,重寫-initWithLayer:
方法
2、創(chuàng)建UIView類用來放置Layer,在view被添加到父視圖之后給它上面的Layer創(chuàng)建動畫
3、實現(xiàn)動畫代理方法,動畫開始時開啟定時器,定時觸發(fā)重繪
4、動畫屬性的值會隨動畫的執(zhí)行不斷變化,定時器觸發(fā)時獲取當前動畫值,畫出當前的位置
重寫-initWithLayer:
:
- (instancetype)initWithLayer:(id)layer {
if (self = [super initWithLayer:layer]) {
if ([layer isKindOfClass:[CircleLayer class]]) {
self.startAngle = [(CircleLayer *)layer startAngle];
self.endAngle = [(CircleLayer *)layer endAngle];
}
}
return self;
}
CAAnimation生成關(guān)鍵幀是通過拷貝CALayer進行的,在拷貝時,只能拷貝原有的(系統(tǒng)的,非自定義的)屬性,不能拷貝自定義的屬性或持有的對象等等,因此需要重載initWithLayer來手動拷貝我們需要拷貝的東西。
創(chuàng)建動畫:
- (void)createAnimationWithKeyPath:(NSString *)key fromValue:(NSNumber *)from toValue:(NSNumber *)to func:(NSString *)func layer:(CALayer *)layer {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:key];
NSNumber *currentAngle = [layer.presentationLayer valueForKey:key];
if (!currentAngle) {
currentAngle = from;
}
anim.fromValue = currentAngle;
anim.toValue = to;
anim.delegate = self;
anim.timingFunction = [CAMediaTimingFunction functionWithName:func];
[layer addAnimation:anim forKey:key];
// 設(shè)置結(jié)束值,這樣動畫結(jié)束之后就會停留在結(jié)束位置,而不會返回初始位置,一定要在添加動畫之后設(shè)置
[layer setValue:to forKey:key];
}
給動畫設(shè)置初始值和結(jié)束值,設(shè)置好動畫執(zhí)行方式,它就會在“暗地里”執(zhí)行,執(zhí)行期間可以通過layer.presentationLayer
獲取當前動畫執(zhí)行到的位置,獲取之后就可以繪制layer了。這樣每秒繪制60次就形成了流暢的動畫效果。
支付寶的記賬本里有一個非??犰诺淖远x控件,現(xiàn)在就模仿一下它的動畫效果:
代碼如下:
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
_animations = [[NSMutableArray alloc] init];
_pieCenter = CGPointMake(frame.size.width/2, frame.size.height/2);
_animationDuration = 3;
_startPieAngle = 0;
_pieLineWidth = 40;
_pieRadius = MIN(frame.size.width/2 - _pieLineWidth, frame.size.width/2 - _pieLineWidth);
_selectedIndex = -1;
_selectedOffsetRadius = 7.0;
}
return self;
}
- (void)reloadData {
[CATransaction begin];
[CATransaction setAnimationDuration:_animationDuration];
CGFloat p = 2 * M_PI;
NSArray *end = @[@(p/5),@(p/4),@(p/3),@(p/2),@(p/1)];
NSArray *start = @[@(0),@(p/5),@(p/4),@(p/3),@(p/2)];
for (int i = 0; i < 5; i ++) {
CircleLayer *layer = [CircleLayer layer];
[self.layer addSublayer:layer];
CGFloat startAngle = [start[i] doubleValue];
CGFloat endAngle = [end[i] doubleValue];
layer.startAngle = startAngle;
layer.endAngle = endAngle;
layer.lineWidth = 30;
layer.fillColor = [UIColor clearColor].CGColor;
layer.strokeColor = [UIColor colorWithHue:((i/8)%20)/20.0+0.02 saturation:(i%8+3)/10.0 brightness:91/100.0 alpha:1].CGColor;
[self createAnimationWithKeyPath:@"startAngle" fromValue:@0 toValue:@(startAngle) layer:layer];
[self createAnimationWithKeyPath:@"endAngle" fromValue:@0 toValue:@(endAngle) layer:layer];
}
[CATransaction commit];
}
- (void)createAnimationWithKeyPath:(NSString *)key fromValue:(NSNumber *)from toValue:(NSNumber *)to layer:(CALayer *)layer {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:key];
NSNumber *currentAngle = [layer.presentationLayer valueForKey:key];
if (!currentAngle) {
currentAngle = from;
}
anim.fromValue = currentAngle;
anim.toValue = to;
anim.delegate = self;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[layer addAnimation:anim forKey:key];
[layer setValue:to forKey:key];
}
- (void)animationDidStart:(CAAnimation *)anim {
if (!_animationTimer) {
static float timeInterval = 1.0/60.0;
_animationTimer= [NSTimer timerWithTimeInterval:timeInterval target:self selector:@selector(timerFired) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_animationTimer forMode:NSRunLoopCommonModes];
}
[_animations addObject:anim];
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
[_animations removeObject:anim];
if (_animations.count == 0) {
[_animationTimer invalidate];
_animationTimer = nil;
}
}
- (void)timerFired {
NSArray *sliceLayerArray = self.layer.sublayers;
[sliceLayerArray enumerateObjectsUsingBlock:^(CircleLayer *layer, NSUInteger idx, BOOL *stop) {
CGFloat currentStartAngle = [[layer.presentationLayer valueForKey:@"startAngle"] doubleValue];
CGFloat currentEndAngle = [[layer.presentationLayer valueForKey:@"endAngle"] doubleValue];
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:_pieCenter radius:_pieRadius startAngle:currentStartAngle endAngle:currentEndAngle clockwise:1];
layer.path = path.CGPath;
}];
}
添加到ViewController上之后,調(diào)用reloadData
就會開始動畫。
仿寫的支付寶記賬本控件效果如下:
完整demo請參考我的GitHub。