你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 仿BOSS直聘APP下拉刷新動畫實現

仿BOSS直聘APP下拉刷新動畫實現

編輯:IOS開發基礎

轉自微信公眾號:iOS面向編碼

BOSS直聘APP的下拉刷新動畫蠻有趣的,我們來嘗試實現一下。

先來看看最終效果:

1469778617354485.gif

關於實現思路:

實現思路這東西,並不是一成不變的,每個人心中都有自己喜歡的思想和套路,這裡僅分享下我的思路,力圖起到拋磚引玉的作用,深入思考,也許你會有更好的方法和思路。

動畫拆分

再復雜的動畫都可以拆分成許多簡單的動畫組合起來,這個動畫大概可以分成兩個主體,我把它分別錄制出來給大家看看

第一個,下拉過程中的動畫

0 (1).gif

第一個動畫又可以拆分為4個大階段,對應著4個點之間的動畫過程:

QQ截圖20160729155053.png

每個大階段又可以拆分為2個小階段(以第一個和第二個點為例):

1)A點到B點之間的動畫:B點不出現,以A點為起點,從A點一直“伸”到B點

2)B點到A點之間的動畫:B點出現,以B點為終點,從A點一直“縮”到B點

綜上,第一個動畫可以拆分為8個階段:

QQ截圖20160729155133.png

第二個,進入刷新狀態的動畫

QQ截圖20160729163527.png

第二個動畫又可以拆分為兩個單獨動畫(旋轉+移動)的組合:

整體旋轉動畫:整體不斷重復360度旋轉

點反復移動動畫:4個點在旋轉360的周期內進行(內->外->內->外)的移動

動畫實現方式

了解了動畫的過程,我們來選擇動畫的實現方式,由於這裡僅需要畫圓形,我們選擇CAShapeLayer來實現。

CAShapeLayer的簡介:

CAShapeLayer顧名思義,就是代表一個形狀(Shape)的Layer,它是CALayer的子類。

CAShapeLayer初始化需要指定Frame,但它的形狀是由path屬性來決定,且必須指定path,不然會沒有形狀。

CAShapeLayer的重要屬性:

1、lineWidth 渲染線的寬度

2、lineCap、lineJoin 渲染線兩端和轉角的樣式

3、fillColor、strokeColor 填充、描邊的渲染顏色

4、path 指定的繪圖路徑,path不完整會自動封閉區域

5、strokeStart、strokeEnd 繪制path的起始和結束的百分比

CAShapeLayer的動畫特點:

1、CAShapeLayer跟CALayer一樣自帶動畫效果

2、CAShapeLayer的動畫效果僅限沿路徑變化,不支持填充區域的動畫效果

動畫實現

我們自定義一個RefreshHeaderView,並通過分類將其和scrollView關聯,當進行下拉操作的時候,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的開始和結束位置,並在適當的時候顯示或隱藏對應的點

- (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 && 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)達到條件時進入刷新狀態

進入刷新狀態的條件:下拉長度超過我們指定的長度,且手已離開屏幕(即scrollView沒有處於拖動的狀態),且沒有正在播放Loading動畫。

進入刷新狀態時,同時執行下拉刷新時需要執行的操作(如加載網絡數據等等)

//如果不是正在刷新,則漸變動畫
if (!self.animating) {
if (progress >= SURefreshPullLen) {
self.y = - (SURefreshPullLen - (SURefreshPullLen - SURefreshHeaderHeight) / 2);
}else {
if (progress = SURefreshPullLen && !self.animating && !self.scrollView.dragging) {
[self startAni];
if (self.handle) {
self.handle();
}
}

執行Loading動畫,我們采用CA動畫來實現

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)回復初始狀態

當用戶拖動的長度達不到臨界值,或者結束Loading的狀態時,RefreshHeaderView移除所有的動畫,回復到初始狀態

- (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;
}];
}

動畫添加

我們創建一個UIScrollView的分類,添加一個給ScrollView添加RefreshHeader的方法

- (void)addRefreshHeaderWithHandle:(void (^)())handle {
SURefreshHeader * header = [[SURefreshHeader alloc]init];
header.handle = handle;
self.header = header;
[self insertSubview:header atIndex:0];
}

需要注意的是,由於分類中不能直接添加Property,我們采用關聯對象的方法將RefreshHeader和ScrollView綁定

objc_setAssociatedObject(self, @selector(header), header, OBJC_ASSOCIATION_ASSIGN);

思考:這裡為什麼用ASSIGN這個關聯策略

此外,由於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:^{
//請求網絡數據
}];
//請求完成後
[tableView.header endRefreshing];

Demo

本文的demo在我的github上可以下載:https://github.com/DaMingShen/SURefresh

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved