自定義MJRefresh:“什么值得買”的下拉刷新實現

寫在前面

“什么值得買”是我這種剁手族常用的軟件,最近發(fā)現它的下拉刷新做得挺好的,而且也算是一種經常見到的樣式,正好前幾天剛好分析完了MJRefresh,趁熱打鐵,這次就來嘗試實現一下它的下拉刷新吧。

一、總體構成

原廠效果圖

從上圖可以看出,RefreshView主要是兩部分組成:

  • 位于上方的Label
  • 位于下方的ImageView

Label就不多介紹了,我們來重點看一下Image部分。

  • 下拉過程中,Circle可以隨著我們下拉的位移量改變
  • 刷新過程中,缺了一角的Circle會圍繞“值”旋轉
  • 刷新完畢后,動畫結束

實現難度不大,下面我們就開始動手吧。

二、刷新部分

最終實現效果

最后的效果大概就是這個樣子的,還算合格,我們來詳細分析下。

(一)資源

提取ipa包內圖片資源的方法有很多,而我這人比較懶,所以喜歡直接用工具,這里推薦給大家一款我一直用的:iOS-Images-Extractor,國內的某Coder寫的,很好用,分享給大家,好用的話別忘了點個星,是給作者最大的鼓舞。

iOS-Images-Extractor使用界面

使用方法很簡單,把ipa包拖進去,點擊start等待分析完成,之后點擊Output Dir就會自動跳轉到輸出目錄。


OK,工具介紹完,我已經把圖片找出來了,一共倆:

看到這倆角色,就明了了,一開始我以為缺了一塊的Circle是用ShapeLayer畫的,原來是美工做的,那就直接用吧,省事。

(二)動畫實現

圖片"zhi"在下,"circle"在上,然后對circle做旋轉動畫就OK了。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.logoView addSubview:self.circleView];
    [self.view addSubview:self.logoView];
}

- (void)viewDidLayoutSubviews{
    self.logoView.center = self.view.center;
    self.logoView.bounds = CGRectMake(0, 0, 30, 30);
    self.circleView.frame = self.logoView.bounds;
}

- (void)viewDidAppear:(BOOL)animated{
    [self.circleView.layer addAnimation:[self getTransformAnimation] forKey:nil];
}

-(CABasicAnimation *)getTransformAnimation{
    CABasicAnimation *animation   = [CABasicAnimation animationWithKeyPath:@"transform.rotation"]; //指定對transform.rotation屬性做動畫
    animation.duration            = 2.0f; //設定動畫持續(xù)時間
    animation.byValue             = @(M_PI*2); //設定旋轉角度,單位是弧度
    animation.fillMode            = kCAFillModeForwards;//設定動畫結束后,不恢復初始狀態(tài)之設置一
    animation.repeatCount         = 1000;//設定動畫執(zhí)行次數
    animation.removedOnCompletion = NO;//設定動畫結束后,不恢復初始狀態(tài)之設置二
    return animation;
}

- (UIImageView *)logoView{
    if (!_logoView) {
        _logoView = [[UIImageView alloc] init];
        _logoView.image = [UIImage imageNamed:@"zhi"];
    }
    return _logoView;
}

- (UIImageView *)circleView{
    if (!_circleView) {
        _circleView = [[UIImageView alloc] init];
        _circleView.image = [UIImage imageNamed:@"circle"];
    }
    return _circleView;
}

很簡單,主要就是動畫部分,如果對動畫不熟悉的童鞋,推薦ios核心動畫高級技巧

三、下拉部分

這部分,主要是要實現Circle隨我們手勢改變自身完成度,先上效果圖:

實現效果圖

(一) 用ShapeLayer畫個圓:

這里的Circle部分,我們用CAShapeLayer來做:

CAShapeLayer是一個通過矢量圖形而不是bitmap來繪制的圖層子類。你指定諸如顏色和線寬等屬性,用CGPath來定義想要繪制的圖形,最后CAShapeLayer就自動渲染出來了。

形象點來說,就是你給CAShapeLayer指定腳本(Path),并設定好各屬性(Color,Width)之后,CAShapeLayer就自動完成了。

-(CAShapeLayer *)getShape{
    UIBezierPath *path       = [UIBezierPath bezierPathWithOvalInRect:self.logoView.bounds];//先寫劇本

    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path          = path.CGPath;//安排劇本
    shapeLayer.fillColor     = [UIColor clearColor].CGColor;//填充色要為透明,不然會遮擋下面的圖層
    shapeLayer.strokeColor   = [UIColor redColor].CGColor;
    shapeLayer.lineWidth     = 1.0;
    shapeLayer.frame         = self.logoView.bounds;
    return shapeLayer;
}

- (void)viewDidAppear:(BOOL)animated{
    [self.logoView.layer addSublayer:[self getShape]]; //將ShapeLayer圖層增加到logoView上
}
畫個圓

(二)控制ShapeLayer的繪制進度

圓畫完了,下面是和Slider.value關聯,讓我們能控制圓的繪制進度。
關鍵屬性:strokeStart,strokeEnd

  • strokeStart:從哪開始繪制
  • strokeEnd:在哪結束繪制

我們設定我們的圓起始點為:

    shapeLayer.strokeStart   = 0;
    shapeLayer.strokeEnd     = 0.9;
stroke起始點

可以出,stroke屬性的特點:

  • 單位是百分比
  • 0點在Layer右側中心
  • 順時針繪制

有了這個屬性,我們就可以很方便的實現我們的目標了。
我們把strokeEnd的初始值設為0,再與我們的Slider.value掛鉤就好了。

完整代碼:

- (void)viewDidAppear:(BOOL)animated{
    [self.logoView.layer addSublayer:self.circleLayer];
}

- (CALayer *)circleLayer{
    if (!_circleLayer) {
        _circleLayer = [self getShape];
    }
    return _circleLayer;
}

- (CAShapeLayer *)getShape{
    UIBezierPath *path       = [UIBezierPath bezierPathWithOvalInRect:self.logoView.bounds];
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.fillColor     = [UIColor clearColor].CGColor;
    shapeLayer.strokeColor   = [UIColor redColor].CGColor;
    shapeLayer.lineWidth     = 1.0;
    shapeLayer.path          = path.CGPath;
    shapeLayer.frame         = self.logoView.bounds;
    shapeLayer.strokeEnd     = 0;
    return shapeLayer;
}

- (IBAction)didSlide:(UISlider *)sender {
    self.circleLayer.strokeEnd = sender.value;
}

四、自定義MJRefresh

經過上面兩個步驟,我們已經實現了下拉刷新的核心視圖動畫,接下來該自定義MJRefresh了。
老規(guī)矩,先上完成圖:


完成效果圖

自定義MJRefreshHeader,需要繼承自MJRefreshHeader,看過我之前文章的小伙伴一定很熟悉了。
不熟悉也不要緊,不過就有點死記硬背的感覺了。

(一)布局

#pragma mark - Const
CGRect kZZZLogoViewBounds = {0,0,25,25};
#pragma mark 在這里做一些初始化配置(比如添加子控件)
- (void)prepare
{
    [super prepare];
    [self.logoView addSubview:self.circleView];
    [self.logoView.layer addSublayer:self.circleLayer];

    [self addSubview:self.logoView];
}

#pragma mark 在這里設置子控件的位置和尺寸

- (void)placeSubviews
{
    [super placeSubviews];
    self.logoView.center = CGPointMake(self.mj_w/2.0, self.mj_h/2.0 + 10.0);// +10是為了logoView在中心點往下一點的位置,方便觀看
    self.logoView.bounds = kZZZLogoViewBounds;
    self.circleView.frame = self.logoView.bounds;
}

#pragma mark - setter & getter

- (UIImageView *)logoView{
    if (!_logoView) {
        _logoView = [[UIImageView alloc] init];
        _logoView.image = [UIImage imageNamed:@"zhi"];
    }
    return _logoView;
}

- (UIImageView *)circleView{
    if (!_circleView) {
        _circleView = [[UIImageView alloc] init];
        _circleView.image = [UIImage imageNamed:@"circle"];
        _circleLayer.hidden = YES; //刷新時候的圖片,開始的時候不需要顯示出來
    }
    return _circleView;
}

- (CAShapeLayer *)circleLayer{
    if (!_circleLayer) {
        _circleLayer = [self creatCircleShapeLayerWithBounds:kZZZLogoViewBounds];//跟上面的getShapeLayer方法一樣,不過這里我稍微改寫了原函數,減少依賴
    }
    return _circleLayer;
}

有幾點需要說明的:

  • MJRefresh默認高度是54,如需修改,放在prepare文件中即可:self.mj_h = **
  • prepare方法中,不能放布局相關的內容,因為調用prepare是在視圖初始化的時候,這時候MJRefresh還沒有加入到View Hierarchy
  • placeSubViews方法中,注意MJRefreshView的Frame.origin = (0, -self.mj_h),所以調整Y值的時候注意正負。


    布局

自定義的時候,慢慢來,出了BUG一般是Frame沒設置好,多利用調試工具。

(二)設置動態(tài)響應

我們只需要做兩件事情:

(1)將下拉位移量與我們的strokeEnd屬性關聯

關聯這件事情,MJRefresh已經幫我們處理了前半部分,我們只需要在相應方法里寫個等式就可以了。
?

(2) 處理狀態(tài)

  • Idle :我們要設置各個組件是否隱藏
  • Pulling: 不需要處理
  • Refreshing:把CircleLayer隱藏,把CircleView顯示并做旋轉動畫

注意的是,我們的需要在endRefreshing方法中,手動移除動畫(因為我們在動畫定義部分為了動畫的流暢性,設置了animation.removedOnCompletion = NO),不然CircleView上的動畫會一直運行。

#pragma mark 監(jiān)聽控件的刷新狀態(tài)
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState;
    
    switch (state) {
        case MJRefreshStateIdle:
            self.circleView.hidden = YES;
            self.circleLayer.hidden = NO;
            break;
        case MJRefreshStatePulling:
            break;
        case MJRefreshStateRefreshing:
            self.circleView.hidden = NO;
            self.circleLayer.hidden = YES;
            [self.circleView.layer addAnimation:[self creatTransformAnimation] forKey:nil];
            break;
        default:
            break;
    }
}

- (void)setPullingPercent:(CGFloat)pullingPercent
{
        self.circleLayer.strokeEnd = pullingPercent;
}

- (void)endRefreshing{


    [self.circleView.layer removeAllAnimations];
    [super endRefreshing];
}

來看一下運行結果:

對比原版,貌似有幾點問題:

  • Refreshing狀態(tài)的時候,CircleLayer的消失做了一個動畫
  • Refreshing結束的時候,CirCleLayer因為和PullingPersent的關聯,strokeEnd直接設為了0

有問題,就解決問題唄。

第一個問題
self.circleLayer.hidden = YES;

問題出在這行代碼上。
這涉及到了CoreAnimation的隱式動畫部分,說白了,你對Layer做的屬性修改,會觸發(fā)系統(tǒng)的隱藏動畫,所以我們取消系統(tǒng)隱藏動畫就好了。取消方法如下:

[CATransaction begin];
[CATransaction setDisableActions:YES];
self.circleLayer.hidden = YES;
[CATransaction commit];
運行結果

好的,這個問題已經不是問題了。

第二個問題

原廠的動畫是,刷新完成之后,CircleLayer要保持StrokeEnd = 1.0的狀態(tài)。

也就是說,需要個參數,能區(qū)分進入Idle狀態(tài)之前是否刷新過,那我們就加個參數唄。

改動部分代碼如下:


- (void)prepare
{
    [super prepare];
    [self.logoView addSubview:self.circleView];
    [self.logoView.layer addSublayer:self.circleLayer];
    [self addSubview:self.logoView];
    self.hasRefreshed = NO;//初始化的時候,肯定是沒有刷新過的
}

#pragma mark 監(jiān)聽控件的刷新狀態(tài)
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState;
    
    switch (state) {
        case MJRefreshStateIdle:
            self.circleView.hidden = YES;
            self.circleLayer.hidden = NO;
            break;
        case MJRefreshStatePulling:
            break;
        case MJRefreshStateRefreshing:
            
            [CATransaction begin];
            [CATransaction setDisableActions:YES];
            self.circleLayer.hidden = YES;
            [CATransaction commit];
        
            self.circleView.hidden = NO;
            [self.circleView.layer addAnimation:[self creatTransformAnimation] forKey:nil];

            self.hasRefreshed = YES;//刷新過了
            break;
        default:
            break;
    }
}

#pragma mark 監(jiān)聽拖拽比例(控件被拖出來的比例)

- (void)setPullingPercent:(CGFloat)pullingPercent
{
    if (self.hasRefreshed) {//刷新返回的時候,strokeEnd = 1.0 
        [CATransaction begin];
        [CATransaction setDisableActions:YES];
        self.circleLayer.strokeEnd = 1.0;
        [CATransaction commit];
        self.hasRefreshed = NO;//重置狀態(tài)為未刷新
    }else{
        self.circleLayer.strokeEnd = pullingPercent;
    }
}

搞定。

總結

MJRefresh給我們提供了很好的底層實現,我們可以在它的基礎上,進行豐富的自定義,基本都能滿足自己的需求。
哪怕是實在滿足不了你了,也可以借鑒MJRefresh的整體思路,自己寫一個簡單的框架。

我在分析完MJRefresh的技術細節(jié)之后,不再感覺自己面對的是一個黑匣子,修改起來是相當地輕松。
所以,讀源碼果然是提高自己技術水平的有效手段(就是有點累)。

至此,MJRefresh的旅程就算結束了。

希望大家以后都能做出獨具個性的刷新控件。

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

相關閱讀更多精彩內容

  • 發(fā)現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 15,056評論 4 61
  • 作者:【美】阿西莫格魯【美】羅賓遜 譯者:李增鋼 出版社:湖南科學技術出版社 作者介紹:阿西莫格魯是麻省理工大學經...
    哈皮波閱讀 1,901評論 1 3
  • 01 我給了自己半個小時平靜的時間,做了個與時間賽跑的重要決定,然后騎上小黃蜂飛快地奔向第一個站點,那里是我戰(zhàn)斗的...
    迷小希閱讀 280評論 3 5
  • 千里共良宵,良宵一刻值千金。 呵呵,其實此刻累得跟個孫子是的。 每到到午夜時分夜深人靜的時候,開車巡游四處攬客的我...
    老趙愛生活閱讀 256評論 0 0
  • 風比陽光還要溫暖 一點一滴蹭化了冬日的堅冰 大地褪去了雪白的偽裝 星星點點間都顯現出生命的痕跡 我靜靜地望著天邊的...
    廣意_閱讀 249評論 0 2

友情鏈接更多精彩內容