iOS動畫篇:下拉刷新動畫

BOSS直聘APP的下拉刷新動畫蠻有趣的殴蓬,我們來嘗試實現(xiàn)一下卸留。

先來看看最終效果:
SURefresh_Finally.gif
關于實現(xiàn)思路:

實現(xiàn)思路這東西趟章,并不是一成不變的堤瘤,每個人心中都有自己喜歡的思想和套路玫芦,這里僅分享下我的思路,力圖起到拋磚引玉的作用本辐,深入思考桥帆,也許你會有更好的方法和思路。

動畫拆分

再復雜的動畫都可以拆分成許多簡單的動畫組合起來慎皱,這個動畫大概可以分成兩個主體老虫,我把它分別錄制出來給大家看看
第一個,下拉過程中的動畫


下拉過程中的動畫.gif

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


4個點.png

每個大階段又可以拆分為2個小階段(以第一個和第二個點為例):
1)A點到B點之間的動畫:B點不出現(xiàn)祈匙,以A點為起點,從A點一直“伸”到B點
2)B點到A點之間的動畫:B點出現(xiàn)天揖,以B點為終點菊卷,從A點一直“縮”到B點

綜上,第一個動畫可以拆分為8個階段:(簡書的圖片怎樣才能橫著排列宝剖?這豎著也太占版面了CACACA)


step1.png

step2.png

step3.png

step4.png

step5.png

step6.png

step7.png

step8.png

step9.png

step10.png

step11.png

step12.png

step13.png

step14.png

第二個洁闰,進入刷新狀態(tài)的動畫


進入刷新狀態(tài)的動畫.gif

第二個動畫又可以拆分為兩個單獨動畫(旋轉+移動)的組合:
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

布吉島布吉島

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市帖旨,隨后出現(xiàn)的幾起案子箕昭,更是在濱河造成了極大的恐慌,老刑警劉巖解阅,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件落竹,死亡現(xiàn)場離奇詭異,居然都是意外死亡货抄,警方通過查閱死者的電腦和手機述召,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碉熄,“玉大人桨武,你說我怎么就攤上這事⌒饨颍” “怎么了呀酸?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長琼梆。 經常有香客問我性誉,道長,這世上最難降的妖魔是什么茎杂? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任错览,我火速辦了婚禮,結果婚禮上煌往,老公的妹妹穿的比我還像新娘倾哺。我一直安慰自己轧邪,他們只是感情好,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布羞海。 她就那樣靜靜地躺著忌愚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪却邓。 梳的紋絲不亂的頭發(fā)上硕糊,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音腊徙,去河邊找鬼简十。 笑死,一個胖子當著我的面吹牛撬腾,可吹牛的內容都是我干的螟蝙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼胶逢,長吁一口氣:“原來是場噩夢啊……” “哼厅瞎!你這毒婦竟也來了饰潜?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤和簸,失蹤者是張志新(化名)和其女友劉穎彭雾,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锁保,經...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡薯酝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了爽柒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吴菠。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖浩村,靈堂內的尸體忽然破棺而出做葵,到底是詐尸還是另有隱情,我是刑警寧澤心墅,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布酿矢,位于F島的核電站,受9級特大地震影響怎燥,放射性物質發(fā)生泄漏瘫筐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一铐姚、第九天 我趴在偏房一處隱蔽的房頂上張望策肝。 院中可真熱鬧,春花似錦、人聲如沸之众。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酝枢。三九已至恬偷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間帘睦,已是汗流浹背袍患。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留竣付,地道東北人诡延。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像古胆,于是被迫代替她去往敵國和親肆良。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內容

  • 發(fā)現(xiàn) 關注 消息 iOS 第三方庫逸绎、插件惹恃、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,093評論 4 62
  • Swift版本點擊這里歡迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh閱讀 25,365評論 7 249
  • 如果今天我把故事講給朋友颊乘,我的朋友一定會覺得“你怎么這么慘”参淹。 是啊,我現(xiàn)在蹲在知味觀的門前乏悄,剛剛看了一個像90年...
    Serenawanwan閱讀 380評論 0 0
  • pp私房菜:番茄海鮮鍋 ?周五公司年會浙值,抽了個五百塊,第一次中獎啊檩小,第一次?拧!识啦!?在糗百過了兩個年會了负蚊,糗百的年會...
    摸魚哥閱讀 508評論 4 2
  • 假如,我就這樣開篇颓哮,會不會覺得太過于唐突家妆,那么,好吧冕茅,我換種方式伤极。 第一次聽sad angel 是在英語課蛹找,課間休...
    北尚閱讀 507評論 0 1