iOS自定義動(dòng)畫(huà)-仿支付寶記賬本

CALayer大部分屬性都可以添加CAAnimation動(dòng)畫(huà),動(dòng)畫(huà)添加到layer上之后就會(huì)自動(dòng)開(kāi)始執(zhí)行,但這僅限于CALayer及其子類已有的屬性,如果是自己添加的屬性,是不會(huì)自動(dòng)產(chǎn)生動(dòng)畫(huà)的,如果需要?jiǎng)赢?huà)效果,就需要自定義動(dòng)畫(huà)了。

如何創(chuàng)建自定義動(dòng)畫(huà)?

大體來(lái)說(shuō)有兩種方法,一種是使用系統(tǒng)的繪制方法畫(huà)出動(dòng)畫(huà),一種是利用定時(shí)器自己繪制動(dòng)畫(huà)。

方法一

1、創(chuàng)建自定義Layer類繼承自CAShapeLayer
2、給Layer添加需要執(zhí)行動(dòng)畫(huà)的屬性,在.m文件中使用@dynamic聲明動(dòng)畫(huà)屬性
3、重寫(xiě)幾個(gè)系統(tǒng)方法:

// 告訴CALayer自定義屬性需要進(jìn)行重繪
+ (BOOL)needsDisplayForKey:(NSString *)key {
    if ([key isEqualToString:@"startAngle"]||[key isEqualToString:@"endAngle"]) {
        return YES;
    }
    return [super needsDisplayForKey:key];
}
// 如果自定義屬性值改變,就生成動(dòng)畫(huà),系統(tǒng)會(huì)自動(dò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];
}

重寫(xiě)上面兩個(gè)方法之后,在外部修改動(dòng)畫(huà)屬性時(shí)就會(huì)觸發(fā)界面重繪。系統(tǒng)會(huì)優(yōu)先調(diào)用-display方法進(jìn)行重繪,如果沒(méi)有重寫(xiě)這個(gè)方法,系統(tǒng)會(huì)調(diào)用-drawInContext:進(jìn)行重繪。要注意:

不在-drawInContext中繪圖的時(shí)候,要在繪圖代碼前后加上:
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
在-drawInContext中繪制的時(shí)候不需要寫(xiě)。

繪制代碼:
- (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)建這個(gè)Layer對(duì)象的時(shí)候,需要給它設(shè)置frame,如果不設(shè)置frame,會(huì)出現(xiàn)繪制失敗。所以最好用-initWithFrame:方法進(jìn)行創(chuàng)建。

方法二

理論上講重繪方法每秒會(huì)被調(diào)用60次,但真機(jī)實(shí)測(cè)發(fā)現(xiàn)只有50多次,并且調(diào)用次數(shù)會(huì)隨著重繪代碼的增多而減少,這會(huì)造成動(dòng)畫(huà)幀數(shù)下降,使動(dòng)畫(huà)看起來(lái)不夠流暢。

第二種方法是創(chuàng)建定時(shí)器,每秒觸發(fā)60次,每次觸發(fā)都進(jìn)行重繪,步驟:
1、創(chuàng)建Layer類繼承自CAShapeLayer,重寫(xiě)-initWithLayer:方法
2、創(chuàng)建UIView類用來(lái)放置Layer,在view被添加到父視圖之后給它上面的Layer創(chuàng)建動(dòng)畫(huà)
3、實(shí)現(xiàn)動(dòng)畫(huà)代理方法,動(dòng)畫(huà)開(kāi)始時(shí)開(kāi)啟定時(shí)器,定時(shí)觸發(fā)重繪
4、動(dòng)畫(huà)屬性的值會(huì)隨動(dòng)畫(huà)的執(zhí)行不斷變化,定時(shí)器觸發(fā)時(shí)獲取當(dāng)前動(dòng)畫(huà)值,畫(huà)出當(dāng)前的位置

重寫(xiě)-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)鍵幀是通過(guò)拷貝CALayer進(jìn)行的,在拷貝時(shí),只能拷貝原有的(系統(tǒng)的,非自定義的)屬性,不能拷貝自定義的屬性或持有的對(duì)象等等,因此需要重載initWithLayer來(lái)手動(dòng)拷貝我們需要拷貝的東西。

創(chuàng)建動(dòng)畫(huà):
- (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é)束值,這樣動(dòng)畫(huà)結(jié)束之后就會(huì)停留在結(jié)束位置,而不會(huì)返回初始位置,一定要在添加動(dòng)畫(huà)之后設(shè)置
[layer setValue:to forKey:key];
}

給動(dòng)畫(huà)設(shè)置初始值和結(jié)束值,設(shè)置好動(dòng)畫(huà)執(zhí)行方式,它就會(huì)在“暗地里”執(zhí)行,執(zhí)行期間可以通過(guò)layer.presentationLayer獲取當(dāng)前動(dòng)畫(huà)執(zhí)行到的位置,獲取之后就可以繪制layer了。這樣每秒繪制60次就形成了流暢的動(dòng)畫(huà)效果。


支付寶的記賬本里有一個(gè)非??犰诺淖远x控件,現(xiàn)在就模仿一下它的動(dòng)畫(huà)效果:

仿支付寶記賬本.gif

代碼如下:
- (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就會(huì)開(kāi)始動(dòng)畫(huà)。

仿寫(xiě)的支付寶記賬本控件效果如下:

6.gif

完整demo請(qǐng)參考我的GitHub

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在iOS中隨處都可以看到絢麗的動(dòng)畫(huà)效果,實(shí)現(xiàn)這些動(dòng)畫(huà)的過(guò)程并不復(fù)雜,今天將帶大家一窺ios動(dòng)畫(huà)全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,699評(píng)論 6 30
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫(huà)效果,實(shí)現(xiàn)這些動(dòng)畫(huà)的過(guò)程并不復(fù)雜,今天將帶大家一窺iOS動(dòng)畫(huà)全貌。在這里你可以看...
    F麥子閱讀 5,275評(píng)論 5 13
  • 轉(zhuǎn)載:http://m.itdecent.cn/p/32fcadd12108 每個(gè)UIView有一個(gè)伙伴稱為l...
    F麥子閱讀 6,602評(píng)論 0 13
  • 每個(gè)UIView有一個(gè)伙伴稱為layer,一個(gè)CALayer。UIView實(shí)際上并沒(méi)有把自己畫(huà)到屏幕上;它繪制本身...
    shenzhenboy閱讀 3,266評(píng)論 0 17
  • 上周與同學(xué)們的同題作文賽,沒(méi)有來(lái)得及與大家分享,現(xiàn)在終于抽出一點(diǎn)兒時(shí)間了。 上課鈴響了,首先主動(dòng)分享習(xí)作的是肖立峰...
    石利蕊閱讀 830評(píng)論 0 3

友情鏈接更多精彩內(nèi)容