MJRefresh源碼解讀

MJRefresh是MJ大神寫的一個(gè)實(shí)現(xiàn)上拉刷新和下拉刷新的第三方庫(kù)健蕊,這個(gè)庫(kù)目前在很多有名的應(yīng)用上都有使用看仇味,下面就來(lái)分析一下MJRefresh的源碼。

1.簡(jiǎn)單應(yīng)用

下面創(chuàng)建一個(gè)綠色的UIScrollview,然后在UIScrollview上加上一個(gè)紅色的視圖作為子視圖:

    self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 20, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
    self.scrollView.backgroundColor = [UIColor greenColor];
    [self.view addSubview:_scrollView];
    
    self.scrollView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(refresh)];
    
    self.scrollView.contentInset = UIEdgeInsetsMake(54, 0, 0, 0);
    
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.scrollView.frame.size.width, self.scrollView.frame.size.height)];
    view.backgroundColor = [UIColor redColor];
    [self.scrollView addSubview:view];

然后我們看一下效果:


Dec-06-2018 15-49-18.gif

在這里我們?cè)O(shè)置的contentInset.top = 54,54正是這個(gè)下拉控件的高度咱圆,所以整個(gè)下拉控件是完全可見(jiàn)的。

2.源碼分析

我們首先看一下MJRefresh的源碼的類的結(jié)構(gòu)功氨,由于上拉刷新和下拉刷新控件的原理基本一致序苏,因此這里我們僅使用下拉刷新控件來(lái)分析:
67C65862-0A9A-4C2D-B157-515C1424A70B.png

下面我們從下拉刷新控件的使用開始來(lái)探索源碼:

    self.scrollView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(refresh)];

首先我們的疑問(wèn)是UIScrollview的mj_header這個(gè)屬性是哪里來(lái)的?我們找到UIScrollview+MJRefresh.h這個(gè)文件捷凄,這是寫的UIScrollview的一個(gè)分類忱详,在這個(gè)分類中我們找到了mj_header屬性,mj_footer屬性纵势,但是分類申明屬性是沒(méi)有set方法和get方法的踱阿,那么怎么去賦值和取值呢?這時(shí)候就要用到runtime的關(guān)聯(lián)屬性方法了钦铁,我們?cè)?code>UIScrollview+MJRefresh.m文件中找到- (void)setMj_header:(MJRefreshHeader *)mj_header方法看看是不是像我猜想的那樣:

F69240D7-E35B-4F2D-99F5-F9A1FEBEFC32.png

事實(shí)證明確實(shí)是這樣,使用關(guān)聯(lián)屬性來(lái)設(shè)值和取值才漆。
MJRefreshNormalHeader最終是繼承自MJRefreshComponent牛曹,那么我們就先從MJRefreshComponent來(lái)看:
首先在MJRefresh.h文件中有一個(gè)枚舉類表示刷新控件的狀態(tài):

/** 刷新控件的狀態(tài) */
typedef NS_ENUM(NSInteger, MJRefreshState) {
    /** 普通閑置狀態(tài) */
    MJRefreshStateIdle = 1,
    /** 松開就可以進(jìn)行刷新的狀態(tài) */
    MJRefreshStatePulling,
    /** 正在刷新中的狀態(tài) */
    MJRefreshStateRefreshing,
    /** 即將刷新的狀態(tài) */
    MJRefreshStateWillRefresh,
    /** 所有數(shù)據(jù)加載完畢,沒(méi)有更多的數(shù)據(jù)了 */
    MJRefreshStateNoMoreData
};

其中默認(rèn)狀態(tài)是MJRefreshStateIdle醇滥, 當(dāng)我們拖拽UIScrollview的時(shí)候在沒(méi)有到達(dá)臨界點(diǎn)之前都是這個(gè)狀態(tài)黎比,當(dāng)我們拖拽到了臨界點(diǎn)之后就變成了MJRefreshStatePulling狀態(tài)超营,這個(gè)狀態(tài)就是松手就可以刷新的狀態(tài),當(dāng)我們?cè)?code>MJRefreshStatePulling狀態(tài)下松手就變成了MJRefreshStateRefreshing狀態(tài)阅虫,即正在刷新?tīng)顟B(tài)演闭。
MJRefreshComponnet中有下列屬性:

  • @property (weak, nonatomic) id refreshingTarget;
    這是回調(diào)的對(duì)象
  • @property (assign, nonatomic) SEL refreshingAction;
    這是回調(diào)的方法
  • @property (assign, nonatomic) MJRefreshState state;
    刷新控件的狀態(tài)
  • @property (assign, nonatomic, readonly) UIEdgeInsets scrollViewOriginalInset;
    scrollview剛開始的contentInset
  • @property (assign, nonatomic) CGFloat pullingPercent;
    拉拽的百分比,用來(lái)控制刷新控件的透明度

MJRefreshComponent.m中的核心方法是:- (void)willMoveToSuperview:(UIView *)newSuperview颓帝,這個(gè)方法是在視圖加入父視圖或者改變父視圖的時(shí)候調(diào)用:

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    // 如果不是UIScrollView米碰,不做任何事情
    if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
    
    // 舊的父控件移除監(jiān)聽(tīng)
    [self removeObservers];
    
    if (newSuperview) { // 新的父控件
        // 設(shè)置寬度
        self.mj_w = newSuperview.mj_w;
        // 設(shè)置位置
        self.mj_x = -_scrollView.mj_insetL;
        
        // 記錄UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // 設(shè)置永遠(yuǎn)支持垂直彈簧效果
        _scrollView.alwaysBounceVertical = YES;
        // 記錄UIScrollView最開始的contentInset
        _scrollViewOriginalInset = _scrollView.mj_inset;
        
        // 添加監(jiān)聽(tīng)
        [self addObservers];
    }
}

在這個(gè)方法里面設(shè)置了self.mj_x,self.mj_w, _scrollViewOriginalInset這三個(gè)屬性并且添加了觀察者:

- (void)addObservers
{
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];
    self.pan = self.scrollView.panGestureRecognizer;
    [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];
}

監(jiān)聽(tīng)了scrollview的contentOffset和contentsize的改變,需要根據(jù)contentoffset的改變來(lái)變更刷新控件的狀態(tài)购城。

MJRefreshHeader

接下來(lái)再來(lái)分析一下MJRefreshComponnet的子類MJRefreshHeader這個(gè)類,這個(gè)類是基礎(chǔ)的下拉刷新控件類:
這個(gè)類的頭文件中多了兩個(gè)方法,這兩個(gè)方法都是用來(lái)創(chuàng)建下拉刷新控件的雷酪,不同的是一個(gè)的回調(diào)是block揩慕,另一個(gè)的回調(diào)是@selector:

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
    MJRefreshHeader *cmp = [[self alloc] init];
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    MJRefreshHeader *cmp = [[self alloc] init];
    [cmp setRefreshingTarget:target refreshingAction:action];
    return cmp;
}

在這個(gè)類中確定了上拉刷新控件的mj_y屬性:

- (void)placeSubviews
{
    [super placeSubviews];
    
    // 設(shè)置y值(當(dāng)自己的高度發(fā)生改變了,肯定要重新調(diào)整Y值侮攀,所以放到placeSubviews方法中設(shè)置y值)
    self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}

由于self.ignoredScrollViewContentInsetTop一般是0锣枝,所以一般情況下就有:

self.mj_y = -self.mj_h;

然后我們?cè)賮?lái)看一下一個(gè)最核心的方法:- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change,這個(gè)方法是contentoffset發(fā)生變化時(shí)KVO產(chǎn)生的調(diào)用兰英,

1D9381DB-72F7-47B1-A3DA-CD1EC182D67D.png

上面這個(gè)方法主要是根據(jù)contentoffset進(jìn)行了一系列判斷然后進(jìn)行了狀態(tài)的變化撇叁,那么在這個(gè)類里面還有一個(gè)很重要的方法,就是- (void)setState:(MJRefreshState)state箭昵,我們看看這個(gè)方法里面做了什么:

3E652B04-896E-47EF-8909-1138807859F6.png

總結(jié)一下MJRefreshHeader這個(gè)類税朴,這個(gè)類實(shí)現(xiàn)了兩個(gè)非常重要的方法,一個(gè)是- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change家制,這個(gè)方法是在用手拖拽scrollview導(dǎo)致contentoffset變化的時(shí)候調(diào)用的正林,在這個(gè)方法中會(huì)根據(jù)contentoffset的值來(lái)改變下拉刷新控件的狀態(tài)。這個(gè)類實(shí)現(xiàn)的另一個(gè)很重要的方法是- (void)setState:(MJRefreshState)state颤殴,在- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change中改變屬性觅廓,在- (void)setState:(MJRefreshState)state中根據(jù)屬性的改變來(lái)做具體的事。

MJRefreshStateHeader

**MJRefreshStateHeader繼承自MJRefreshHeader涵但,這個(gè)類是帶有狀態(tài)文字的刷新控件杈绸,沒(méi)有箭頭和菊花。這個(gè)類比較簡(jiǎn)單矮瘟,主要是對(duì)狀態(tài)label和最近刷新時(shí)間label進(jìn)行布局:

920A7894-46D5-4E6E-9DC2-3DBF46196AE6.png

MJRefreshNoramlHeaderMJRefreshStateHeader的子類瞳脓,它是默認(rèn)的下拉刷新控件類,我們實(shí)例中用的就是這種澈侠,這種刷新控件是在拖拽的時(shí)候顯示箭頭劫侧,當(dāng)開始刷新的時(shí)候箭頭小時(shí),顯示菊花。

MJRefreshNoramlHeader這個(gè)類做了兩件事烧栋,一件事是布局上面說(shuō)到的箭頭視圖和菊花視圖写妥,另外一件事是處理在拖拽過(guò)程中箭頭和菊花的變化。

  • 布局菊花和箭頭:


    C88D7D21-C81A-4ED7-8AED-44ABF744559F.png
  • 處理拖拽過(guò)程中箭頭和菊花的變化


    195CF5CF-449C-4514-83DB-5F21BD0D75AF.png
MJRefreshGifHeader

MJRefreshGifHeader這個(gè)類也是繼承自MJRefreshStateHeader审姓,它是帶GIF的下拉刷新控件珍特,就是把MJRefreshNormalHeader的箭頭和菊花變成GIF,下面我們先看一下MJRefreshNormal的簡(jiǎn)單使用:

MJRefreshGifHeader *header = [MJRefreshGifHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewData)];
// Set the ordinary state of animated images
[header setImages:idleImages forState:MJRefreshStateIdle];
// Set the pulling state of animated images(Enter the status of refreshing as soon as loosen)
[header setImages:pullingImages forState:MJRefreshStatePulling];
// Set the refreshing state of animated images
[header setImages:refreshingImages forState:MJRefreshStateRefreshing];
// Set header
self.tableView.mj_header = header;

下面我們就從- (void)setImages:(NSArray *)images forState:(MJRefreshState)state這個(gè)方法下手來(lái)看看具體實(shí)現(xiàn):

- (void)setImages:(NSArray *)images forState:(MJRefreshState)state 
{ 
    [self setImages:images duration:images.count * 0.1 forState:state]; 
}

這個(gè)方法還是主要調(diào)用- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state這個(gè)方法:

- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state 
{ 
    if (images == nil) return; 
    
    self.stateImages[@(state)] = images; 
    self.stateDurations[@(state)] = @(duration); 
    
    /* 根據(jù)圖片設(shè)置控件的高度 */ 
    UIImage *image = [images firstObject]; 
    if (image.size.height > self.mj_h) { 
        self.mj_h = image.size.height; 
    } 
}

在這個(gè)方法中魔吐,使用了兩個(gè)字典扎筒,一個(gè)字典用來(lái)存放各種狀態(tài)下的圖片數(shù)組,因?yàn)镚IF本質(zhì)上也就是多張圖片循環(huán)播放嘛画畅,另一個(gè)字典用來(lái)存放各種狀態(tài)下GIF動(dòng)畫的周期砸琅。并且如果GIF圖片的高度大于刷新控件的高度,那么就調(diào)整刷新控件的高度為GIF圖片的高度轴踱。
我們?cè)賮?lái)看一下GIF視圖的布局:

1CF63EF8-955D-4A2C-97B3-CFDD54DA3B88.png

根據(jù)狀態(tài)變化顯示不同的GIF視圖:
3ED6ABA6-8BFB-458A-9ACD-55D670A78572.png

這篇文章在簡(jiǎn)書的地址:MJRefresh源碼解讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末症脂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子淫僻,更是在濱河造成了極大的恐慌诱篷,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雳灵,死亡現(xiàn)場(chǎng)離奇詭異棕所,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)悯辙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門琳省,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人躲撰,你說(shuō)我怎么就攤上這事针贬。” “怎么了拢蛋?”我有些...
    開封第一講書人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵桦他,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我谆棱,道長(zhǎng)快压,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任垃瞧,我火速辦了婚禮蔫劣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘个从。我一直安慰自己拦宣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開白布信姓。 她就那樣靜靜地躺著鸵隧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪意推。 梳的紋絲不亂的頭發(fā)上豆瘫,一...
    開封第一講書人閱讀 51,198評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音菊值,去河邊找鬼外驱。 笑死,一個(gè)胖子當(dāng)著我的面吹牛腻窒,可吹牛的內(nèi)容都是我干的昵宇。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼儿子,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼瓦哎!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起柔逼,我...
    開封第一講書人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蒋譬,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后愉适,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體犯助,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年维咸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了剂买。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡癌蓖,死狀恐怖瞬哼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情费坊,我是刑警寧澤倒槐,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站附井,受9級(jí)特大地震影響讨越,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜永毅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一把跨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沼死,春花似錦着逐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)健芭。三九已至,卻和暖如春秀姐,著一層夾襖步出監(jiān)牢的瞬間慈迈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工省有, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留痒留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓蠢沿,卻偏偏與公主長(zhǎng)得像伸头,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子舷蟀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354