
效果展示.gif
預(yù)備知識
實現(xiàn)分析
- 整個動畫的實現(xiàn)可以分為兩個部分:
- 圓環(huán)放大的過程,如何保證圓環(huán)放大的時候?qū)挾炔蛔?/li>
- 進度條繪制的過程
- 整個過程主要依靠CADisplaylink被觸發(fā)時調(diào)用的方法中去做重繪來實現(xiàn)。
步驟1 :
????????CADisplaylink 是一個計時器對象,可以使用這個對象來保持應(yīng)用中的繪制與顯示刷新的同步。假設(shè)displaylink綁定的方法名字為A方法,在沒有卡頓時,iOS 設(shè)備屏幕顯示每秒刷新60次,意味著 displaylink的屬性frameInterval 為默認(rèn)值時,每秒回調(diào)60次A方法,當(dāng)frameInterval 改為2時,每秒回調(diào)30(60/2)次A方法。假定放大過程設(shè)定為0.2秒,則整個放大過程調(diào)用的A方法次數(shù)為12次。圓環(huán)最大時半徑為x,初始半徑為y,則每次調(diào)用方法時圓環(huán)的半徑為r = (x - y)/12 + y。在A方法中用貝塞爾曲線繪制半徑為r,linewidth為需要的寬度的圓環(huán)就可以達到效果。
步驟2:
????????第二步是進度條繪制的過程。進度條繪制主要依靠的是CAShapeLayer的strokeStart和strokeEnd屬性。首先需要提前繪制好進度條layer的路徑,也就是紅色圓圈所在的路徑。strokeStart和strokeEnd分別表示path的start位置和end位置,取值范圍為[0,1]。假設(shè)strokeStart為0,strokeEnd為1,那么繪制出來的就是一條完整的路徑,strokeStart為0,strokeEnd為0.5,那么繪制出來的就是完整路徑的一半。同時這兩個屬性都支持動畫。然而我們這里用CADisPlayLink,只要直接改值就好。沒錯,只要在初始化的時候繪制好路徑,并且給strokeStart和strokeEnd屬性都賦值為0,這樣就不會繪制出進度條。然后在displayLink調(diào)用A方法的時候,逐漸將strokeEnd加上delta,delta = 1 / (進度條持續(xù)時間 * 60),就可以實現(xiàn)進度條的動畫。
具體實現(xiàn)
//最大錄制時間
static float const maxRecordTime = 10.f;
//初始半徑
static float const startRadius = 35.f;
//最大半徑
static float const endRadius = 50.f;
//圓圈寬度
static float const lineWidth = 6.f;
@interface KiraCircleButton ()
@property (nonatomic, strong) CAShapeLayer *circleLayer;//白色圓圈圖層
@property (nonatomic, strong) CAShapeLayer *maskLayer;//遮罩圖層
@property (nonatomic, strong) CAShapeLayer *drawLayer;//進度條繪制圖層
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, assign) CGFloat currentRadius; //當(dāng)前半徑
@property (nonatomic, strong) UIVisualEffectView* effectView;
@property (nonatomic, assign) CGPoint centerPoint;
@property (nonatomic, assign) float currentRecordTime;
@end
- 首先初始化View并且給view加上手勢,本文的動畫是通過長按的手勢觸發(fā)的。同時計算好進度條動畫的路徑,提前添加layer到view上,將strokeStart和strokeEnd屬性都賦為0。
- (void)initUI {
self.centerPoint = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2);
[self setUserInteractionEnabled:YES];
//添加手勢
UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doSomeThingWhenTap)];
[self addGestureRecognizer:tap];
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(doSomeThingWhenLongTap:)];
longPress.minimumPressDuration = 0.2;
longPress.delegate = self;
[self addGestureRecognizer:longPress];
//給按鈕增加毛玻璃效果
UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleRegular];
self.effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
self.effectView.userInteractionEnabled = NO;
[self addSubview:self.effectView];
[self.effectView setFrame:CGRectMake(startRadius - endRadius, startRadius - endRadius, 2 * endRadius, 2 * endRadius)];
UIBezierPath *backPath = [UIBezierPath bezierPathWithArcCenter:self.centerPoint radius:endRadius - lineWidth/2 startAngle:- M_PI_2 endAngle:3 * M_PI_2 clockwise:YES];
CAShapeLayer *secondLayer = [CAShapeLayer layer];
secondLayer.strokeColor = [UIColor colorWithRed:1 green:64.f/255.f blue:64.f/255.f alpha:1].CGColor;
secondLayer.lineWidth = lineWidth;
secondLayer.fillColor = [UIColor clearColor].CGColor;
secondLayer.path = backPath.CGPath;
secondLayer .strokeStart = 0;
secondLayer.strokeEnd = 0;
_drawLayer = secondLayer;
_circleLayer = [CAShapeLayer layer];
_maskLayer = [CAShapeLayer layer];
[self resetCaptureButton];
[self.layer addSublayer:_circleLayer];
[self.layer setMask:_maskLayer];
[self.layer addSublayer:secondLayer];
}
- 然后在長按調(diào)用的方法中,初始化displaylink,綁定changeRedius方法到di splayLink。將displaylink添加到當(dāng)前的runloop中,然后將displaylink的paused置為NO。
- (void)doSomeThingWhenLongTap:(UILongPressGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateBegan) {
NSLog(@"Im long tapped start");
self.displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(changeRadius)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
self.displayLink.paused = NO;
if (self.delegate && [self.delegate respondsToSelector:@selector(startProgress)]) {
[self.delegate startProgress];
}
} else if (gesture.state == UIGestureRecognizerStateEnded) {
NSLog(@"Im long tapped end");
//end
self.displayLink.paused = YES;
if (self.delegate && [self.delegate respondsToSelector:@selector(endProgress)]) {
[self.delegate endProgress];
}
[self resetCaptureButton];
}
}
- 最后是關(guān)鍵的changeRadius方法,也就是A方法。每次displaylink被觸發(fā)都將調(diào)用changeRadius方法,在currentRadius增加到最大值之前執(zhí)行放大動畫,在到達最大值之后就是進度條繪制的動畫了。
- (void)changeRadius {
CGFloat toValue = endRadius - lineWidth/2;
CGFloat fromValue = startRadius - lineWidth/2;
CGFloat duration = 0.2;
CGFloat times = 60 * duration;
CGFloat delta = (toValue - fromValue) / times;
_currentRecordTime += 1.f/60;
_currentRadius += delta;
if (_currentRadius <= toValue) {
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:self.centerPoint radius: _currentRadius startAngle:0 endAngle:M_PI * 2 clockwise:YES];
_circleLayer.path = path.CGPath;
_circleLayer.lineWidth = lineWidth;
UIBezierPath *maskPath = [UIBezierPath bezierPathWithArcCenter:self.centerPoint radius:_currentRadius + 2 startAngle:0 endAngle:M_PI * 2 clockwise:YES];
_maskLayer = [CAShapeLayer layer];
_maskLayer.path = maskPath.CGPath;
[self.layer setMask:_maskLayer];
} else {
CGFloat delta = 1 / (maxRecordTime * 60);
self.drawLayer.strokeEnd += delta;
if (self.drawLayer.strokeEnd >= 1) {
self.displayLink.paused = YES;
if (self.delegate && [self.delegate respondsToSelector:@selector(endProgress)]) {
[self.delegate endProgress];
}
}
}
}
最后
附上 demo鏈接 ,demo實現(xiàn)了上述的效果,但是并沒有封裝成一個復(fù)用性比較高的控件,主要以提供實現(xiàn)思路為主。有任何疑問歡迎指出??
補充更新: 2019/02/17
- 勘誤:CADisplayLink 的刷新頻率確實是60次/秒左右,但是并不固定,由于每次調(diào)用 CADisplayLink 的時間間隔都不是平均的,所以我們不能根據(jù)調(diào)用次數(shù)乘以1/60的時間間隔來得到當(dāng)前經(jīng)歷的時間。正確計算當(dāng)前經(jīng)歷時間的方法是通過獲取當(dāng)前時間再減去起始時間來得到。
- 擴展:出于 KiraCircleButton Demo的易用性、可維護性、擴展性考慮,我對代碼進行了更新,動畫支持配置不同緩動函數(shù),Demo地址不變,如果需要看第一版的代碼的話可以切到最初的提交閱讀。有關(guān)新的 KiraCircleButton 的設(shè)計實現(xiàn)介紹請點擊CADisplayLink 動畫進階