BOSS直聘APP的下拉刷新動畫蠻有趣的殴蓬,我們來嘗試實現(xiàn)一下卸留。
先來看看最終效果:
關于實現(xiàn)思路:
實現(xiàn)思路這東西趟章,并不是一成不變的堤瘤,每個人心中都有自己喜歡的思想和套路玫芦,這里僅分享下我的思路,力圖起到拋磚引玉的作用本辐,深入思考桥帆,也許你會有更好的方法和思路。
動畫拆分
再復雜的動畫都可以拆分成許多簡單的動畫組合起來慎皱,這個動畫大概可以分成兩個主體老虫,我把它分別錄制出來給大家看看
第一個,下拉過程中的動畫
第一個動畫又可以拆分為4個大階段茫多,對應著4個點之間的動畫過程:
每個大階段又可以拆分為2個小階段(以第一個和第二個點為例):
1)A點到B點之間的動畫:B點不出現(xiàn)祈匙,以A點為起點,從A點一直“伸”到B點
2)B點到A點之間的動畫:B點出現(xiàn)天揖,以B點為終點菊卷,從A點一直“縮”到B點
綜上,第一個動畫可以拆分為8個階段:(簡書的圖片怎樣才能橫著排列宝剖?這豎著也太占版面了CACACA)
第二個洁闰,進入刷新狀態(tài)的動畫
第二個動畫又可以拆分為兩個單獨動畫(旋轉+移動)的組合:
1)整體旋轉動畫:整體不斷重復360度旋轉
2)點反復移動動畫:4個點在旋轉360的周期內進行(內->外->內->外)的移動
動畫實現(xiàn)方式
了解了動畫的過程,我們來選擇動畫的實現(xiàn)方式万细,由于這里僅需要畫圓形扑眉,我們選擇CAShapeLayer來實現(xiàn)。
CAShapeLayer的簡介:
CAShapeLayer顧名思義赖钞,就是代表一個形狀(Shape)的Layer腰素,它是CALayer的子類。
CAShapeLayer初始化需要指定Frame雪营,但它的形狀是由path屬性來決定弓千,且必須指定path,不然會沒有形狀献起。
CAShapeLayer的重要屬性:
1洋访、lineWidth 渲染線的寬度
2、lineCap谴餐、lineJoin 渲染線兩端和轉角的樣式
3姻政、fillColor、strokeColor 填充岂嗓、描邊的渲染顏色
4汁展、path 指定的繪圖路徑,path不完整會自動封閉區(qū)域
5、strokeStart食绿、strokeEnd 繪制path的起始和結束的百分比
CAShapeLayer的動畫特點:
1侈咕、CAShapeLayer跟CALayer一樣自帶動畫效果
2、CAShapeLayer的動畫效果僅限沿路徑變化器紧,不支持填充區(qū)域的動畫效果
動畫實現(xiàn)
我們自定義一個RefreshHeaderView耀销,并通過分類將其和scrollView關聯(lián),當進行下拉操作的時候品洛,headerView進行相應的動畫树姨。
1)固定位置的4個點
對應4個Layer摩桶,Layer的路徑是圓形桥状,填充顏色和路徑顏色一致
CGPoint topPoint = CGPointMake(centerLine, radius);
self.TopPointLayer = [self layerWithPoint:topPoint color:topPointColor];
self.TopPointLayer.hidden = NO;
self.TopPointLayer.opacity = 0.f;
[self.layer addSublayer:self.TopPointLayer];
CGPoint leftPoint = CGPointMake(radius, centerLine);
self.LeftPointLayer = [self layerWithPoint:leftPoint color:leftPointColor];
[self.layer addSublayer:self.LeftPointLayer];
CGPoint bottomPoint = CGPointMake(centerLine, SURefreshHeaderHeight - radius);
self.BottomPointLayer = [self layerWithPoint:bottomPoint color:bottomPointColor];
[self.layer addSublayer:self.BottomPointLayer];
CGPoint rightPoint = CGPointMake(SURefreshHeaderHeight - radius, centerLine);
self.rightPointLayer = [self layerWithPoint:rightPoint color:rightPointColor];
[self.layer addSublayer:self.rightPointLayer];
- (CAShapeLayer *)layerWithPoint:(CGPoint)center color:(CGColorRef)color {
CAShapeLayer * layer = [CAShapeLayer layer];
layer.frame = CGRectMake(center.x - SURefreshPointRadius, center.y - SURefreshPointRadius, SURefreshPointRadius * 2, SURefreshPointRadius * 2);
layer.fillColor = color;
layer.path = [self pointPath];
layer.hidden = YES;
return layer;
}
- (CGPathRef)pointPath {
return [UIBezierPath bezierPathWithArcCenter:CGPointMake(SURefreshPointRadius, SURefreshPointRadius) radius:SURefreshPointRadius startAngle:0 endAngle:M_PI * 2 clockwise:YES].CGPath;
}
2)4個點的連接介質
對應一個Layer,Layer的路徑是由4段直線拼接而成硝清,直線的直徑和圓形的直接一致辅斟,初始的渲染結束位置為0。
8個階段的動畫芦拿,可以看成是Layer的渲染開始和結束位置不斷變化士飒,并通過改變其渲染的起始和結束位置來改變其形狀
self.lineLayer = [CAShapeLayer layer];
self.lineLayer.frame = self.bounds;
self.lineLayer.lineWidth = SURefreshPointRadius * 2;
self.lineLayer.lineCap = kCALineCapRound;
self.lineLayer.lineJoin = kCALineJoinRound;
self.lineLayer.fillColor = topPointColor;
self.lineLayer.strokeColor = topPointColor;
UIBezierPath * path = [UIBezierPath bezierPath];
[path moveToPoint:topPoint];
[path addLineToPoint:leftPoint];
[path moveToPoint:leftPoint];
[path addLineToPoint:bottomPoint];
[path moveToPoint:bottomPoint];
[path addLineToPoint:rightPoint];
[path moveToPoint:rightPoint];
[path addLineToPoint:topPoint];
self.lineLayer.path = path.CGPath;
self.lineLayer.strokeStart = 0.f;
self.lineLayer.strokeEnd = 0.f;
[self.layer insertSublayer:self.lineLayer above:self.TopPointLayer];
3)滑動過程控制動畫進度
該步驟的核心是通過下拉的長度計算LineLayer的開始和結束位置,并在適當?shù)臅r候顯示或隱藏對應的點
- (void)setLineLayerStrokeWithProgress:(CGFloat)progress {
float startProgress = 0.f;
float endProgress = 0.f;
//沒有下拉蔗崎,隱藏動畫
if (progress < 0) {
self.TopPointLayer.opacity = 0.f;
[self adjustPointStateWithIndex:0];
}
//下拉前奏:頂部的Point的可見度漸變的過程
else if (progress >= 0 && progress < (SURefreshPullLen - 40)) {
self.TopPointLayer.opacity = progress / 20;
[self adjustPointStateWithIndex:0];
}
//開始動畫酵幕,這里將下拉的進度分為4個大階段,方便處理硅急,請看前面的描述
else if (progress >= (SURefreshPullLen - 40) && progress < SURefreshPullLen) {
self.TopPointLayer.opacity = 1.0;
//大階段 0 ~ 3
NSInteger stage = (progress - (SURefreshPullLen - 40)) / 10;
//對應每個大階段的前半段稻励,請看前面描述
CGFloat subProgress = (progress - (SURefreshPullLen - 40)) - (stage * 10);
if (subProgress >= 0 && subProgress <= 5) {
[self adjustPointStateWithIndex:stage * 2];
startProgress = stage / 4.0;
endProgress = stage / 4.0 + subProgress / 40.0 * 2;
}
//對應每個大階段的后半段房轿,請看前面描述
if (subProgress > 5 && subProgress < 10) {
[self adjustPointStateWithIndex:stage * 2 + 1];
startProgress = stage / 4.0 + (subProgress - 5) / 40.0 * 2;
if (startProgress < (stage + 1) / 4.0 - 0.1) {
startProgress = (stage + 1) / 4.0 - 0.1;
}
endProgress = (stage + 1) / 4.0;
}
}
//下拉超過一定長度,4個點已經完全顯示
else {
self.TopPointLayer.opacity = 1.0;
[self adjustPointStateWithIndex:NSIntegerMax];
startProgress = 1.0;
endProgress = 1.0;
}
//計算完畢笔刹,設置LineLayer的開始和結束位置
self.lineLayer.strokeStart = startProgress;
self.lineLayer.strokeEnd = endProgress;
}
- (void)adjustPointStateWithIndex:(NSInteger)index { //index : 小階段: 0 ~ 7
self.LeftPointLayer.hidden = index > 1 ? NO : YES;
self.BottomPointLayer.hidden = index > 3 ? NO : YES;
self.rightPointLayer.hidden = index > 5 ? NO : YES;
self.lineLayer.strokeColor = index > 5 ? rightPointColor : index > 3 ? bottomPointColor : index > 1 ? leftPointColor : topPointColor;
}
4)達到條件時進入刷新狀態(tài)
進入刷新狀態(tài)的條件:下拉長度超過我們指定的長度,且手已離開屏幕(即scrollView沒有處于拖動的狀態(tài))冬耿,且沒有正在播放Loading動畫舌菜。
進入刷新狀態(tài)時,同時執(zhí)行下拉刷新時需要執(zhí)行的操作(如加載網絡數(shù)據(jù)等等)
//如果不是正在刷新亦镶,則漸變動畫
if (!self.animating) {
if (progress >= SURefreshPullLen) {
self.y = - (SURefreshPullLen - (SURefreshPullLen - SURefreshHeaderHeight) / 2);
}else {
if (progress <= self.h) {
self.y = - progress;
}else {
self.y = - (self.h + (progress - self.h) / 2);
}
}
[self setLineLayerStrokeWithProgress:progress];
}
//如果到達臨界點日月,則執(zhí)行刷新動畫
if (progress >= SURefreshPullLen && !self.animating && !self.scrollView.dragging) {
[self startAni];
if (self.handle) {
self.handle();
}
}
執(zhí)行Loading動畫,我們采用CA動畫來實現(xiàn)
scrollView的下沉動畫
[UIView animateWithDuration:0.5 animations:^{
UIEdgeInsets inset = self.scrollView.contentInset;
inset.top = SURefreshPullLen;
self.scrollView.contentInset = inset;
}];
4個點的來回移動動畫
[self addTranslationAniToLayer:self.TopPointLayer xValue:0 yValue:SURefreshTranslatLen];
[self addTranslationAniToLayer:self.LeftPointLayer xValue:SURefreshTranslatLen yValue:0];
[self addTranslationAniToLayer:self.BottomPointLayer xValue:0 yValue:-SURefreshTranslatLen];
[self addTranslationAniToLayer:self.rightPointLayer xValue:-SURefreshTranslatLen yValue:0];
- (void)addTranslationAniToLayer:(CALayer *)layer xValue:(CGFloat)x yValue:(CGFloat)y {
CAKeyframeAnimation * translationKeyframeAni = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
translationKeyframeAni.duration = 1.0;
translationKeyframeAni.repeatCount = HUGE;
translationKeyframeAni.removedOnCompletion = NO;
translationKeyframeAni.fillMode = kCAFillModeForwards;
translationKeyframeAni.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
NSValue * fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, 0, 0.f)];
NSValue * toValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(x, y, 0.f)];
translationKeyframeAni.values = @[fromValue, toValue, fromValue, toValue, fromValue];
[layer addAnimation:translationKeyframeAni forKey:@"translationKeyframeAni"];
}
RefreshHeader的整體旋轉動畫
[self addRotationAniToLayer:self.layer];
- (void)addRotationAniToLayer:(CALayer *)layer {
CABasicAnimation * rotationAni = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotationAni.fromValue = @(0);
rotationAni.toValue = @(M_PI * 2);
rotationAni.duration = 1.0;
rotationAni.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
rotationAni.repeatCount = HUGE;
rotationAni.fillMode = kCAFillModeForwards;
rotationAni.removedOnCompletion = NO;
[layer addAnimation:rotationAni forKey:@"rotationAni"];
}
5)回復初始狀態(tài)
當用戶拖動的長度達不到臨界值缤骨,或者結束Loading的狀態(tài)時山孔,RefreshHeaderView移除所有的動畫,回復到初始狀態(tài)
- (void)removeAni {
[UIView animateWithDuration:0.5 animations:^{
UIEdgeInsets inset = self.scrollView.contentInset;
inset.top = 0.f;
self.scrollView.contentInset = inset;
} completion:^(BOOL finished) {
[self.TopPointLayer removeAllAnimations];
[self.LeftPointLayer removeAllAnimations];
[self.BottomPointLayer removeAllAnimations];
[self.rightPointLayer removeAllAnimations];
[self.layer removeAllAnimations];
[self adjustPointStateWithIndex:0];
self.animating = NO;
}];
}
動畫添加
我們創(chuàng)建一個UIScrollView的分類荷憋,添加一個給ScrollView添加RefreshHeader的方法
- (void)addRefreshHeaderWithHandle:(void (^)())handle {
SURefreshHeader * header = [[SURefreshHeader alloc]init];
header.handle = handle;
self.header = header;
[self insertSubview:header atIndex:0];
}
需要注意的是台颠,由于分類中不能直接添加Property,我們采用關聯(lián)對象的方法將RefreshHeader和ScrollView綁定
objc_setAssociatedObject(self, @selector(header), header, OBJC_ASSOCIATION_ASSIGN);
思考:這里為什么用ASSIGN這個關聯(lián)策略
此外,由于ScrollView銷毀的時候串前,RefreshHeader也銷毀瘫里,但是由于RefreshHeader是ScrollView的觀察者,不移除將導致應用崩潰荡碾,因此在銷毀ScrollView之前需要將觀察者移除谨读,這里采用方法交換在Dealloc方法里面將觀察者移除。
+ (void)load {
Method originalMethod = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc"));
Method swizzleMethod = class_getInstanceMethod([self class], NSSelectorFromString(@"su_dealloc"));
method_exchangeImplementations(originalMethod, swizzleMethod);
}
- (void)su_dealloc {
self.header = nil;
[self su_dealloc];
}
思考:在本代碼中ScrollView坛吁、RefreshHeader劳殖、RefreshBlock三者的引用關系是怎樣的?嘗試畫出一個示意圖拨脉,加深對內存管理的理解哆姻。
到這里,我們就可以使用自己寫的下拉刷新庫應用在工程中了玫膀,就像使用MJRefresh一樣方便矛缨。
[self.tableView addRefreshHeaderWithHandle:^{
//請求網絡數(shù)據(jù)
}];
//請求完成后
[tableView.header endRefreshing];
Demo
本文的demo在我的github上可以下載:GitHub : SURefresh
Next
布吉島布吉島