MJRefresh原理分析

MJRefresh是流行的下拉刷新控件隙券,前段時間為了修復(fù)一個BUG,讀了它的源碼闹司,本文總結(jié)一下實現(xiàn)的原理

下拉刷新的基本原理大部分的下拉刷新控件娱仔,都是用contentInset實現(xiàn)的。默認(rèn)情況下游桩,如果一個UIScrollView的左上角在導(dǎo)航欄的正下方牲迫,那么它的contentInset是64耐朴,而contentOffset是-64。繼續(xù)下拉的話盹憎,contentOffset就會越來越小筛峭,如果上滑,contentOffset就會增大陪每,直到左上角達(dá)到屏幕的左上角時影晓,contentOffset剛好為0

默認(rèn)情況下,如果下拉一個UIScrollView奶稠,在松手之后俯艰,會彈回初始的位置(導(dǎo)航欄下方)。而大部分的下拉刷新控件锌订,都是將自己放在UIScrollView的上方竹握,起始y設(shè)置成負(fù)數(shù),所以平時不會顯示出來辆飘,只有下拉的時候才會出現(xiàn)啦辐,放開又會彈回去。然后在loading的時候蜈项,臨時把contentInset增大芹关,相當(dāng)于把UIScrollView往下擠,于是下拉刷新的控件就會顯示出來紧卒,然后刷新完成之后侥衬,再把contentInset改回原來的值,實現(xiàn)回彈的效果

基本上跑芳,MJRefresh也是這么實現(xiàn)的

創(chuàng)建下拉刷新控件實例

從創(chuàng)建實例的代碼開始:

MJRefreshNormalHeader *header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{ ? ?[myController loadCollectionDataNeedReset:YES withBlock:^{ ? ? ? ?[self.header endRefreshing]; ? ? ? ?[self reloadData]; ? ?}];}];

調(diào)用的是一個工廠方法headerWithRefreshingBlock轴总,這個方法定義在各種header控件的基類MJRefreshHeader里:

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock{ ? ?MJRefreshHeader *cmp = [[self alloc] init]; ? ?cmp.refreshingBlock = refreshingBlock; ? ?return cmp;}

然后會調(diào)用init方法,由于MJRefreshHeader里并沒有定義init方法博个,而它的基類MJRefreshComponent里定義了怀樟,所以會進入到基類的初始化方法里:

- (instancetype)initWithFrame:(CGRect)frame{ ? ?if (self = [super initWithFrame:frame]) { ? ? ? ?// 準(zhǔn)備工作 ? ? ? ?[self prepare]; ? ? ? ?// 默認(rèn)是普通狀態(tài) ? ? ? ?self.state = MJRefreshStateIdle; ? ?} ? ?return self;}

這里的關(guān)鍵是prepare方法,這個方法是第一個擴展點盆佣,具體的header(包括庫提供的原生header往堡,和用戶自定義的header)有哪些屬性,樣式是怎么樣共耍,都是在這個方法里實現(xiàn)的虑灰。每個子類的prepare方法,都會調(diào)用父類的prepare方法痹兜。所以在擴展的時候穆咐,公共的屬性寫在父類的prepare方法里,特有的屬性寫在子類的prepare方法里佃蚜。比如庸娱,我們看一下MJRefreshStateHeader的:

- (void)prepare{ ? ?[super prepare]; ? ?// 初始化文字 ? ?[self setTitle:MJRefreshHeaderIdleText forState:MJRefreshStateIdle]; ? ?[self setTitle:MJRefreshHeaderPullingText forState:MJRefreshStatePulling]; ? ?[self setTitle:MJRefreshHeaderRefreshingText forState:MJRefreshStateRefreshing];}

總之,調(diào)用headerWithRefreshingBlock方法以后谐算,就得到了一個UIView的實例熟尉,也就是下拉刷新的控件。但是現(xiàn)在它還沒有掛到任何superview上洲脂,也沒有任何行為

將下拉刷新控件斤儿,掛到UIScrollView上

接下來的調(diào)用:

self.header = header;

這是利用了UIScrollView+MJRefresh里的一個category,為UIScrollView增加了屬性header和footer恐锦。這里用到了關(guān)聯(lián)對象的技巧(AssociatedObject)往果,因為category通常情況下是不能直接添加實例變量的

- (void)setHeader:(MJRefreshHeader *)header{ ? ?if (header != self.header) { ? ? ? ?// 刪除舊的,添加新的 ? ? ? ?[self.header removeFromSuperview]; ? ? ? ?[self addSubview:header]; ? ? ? ?// 存儲新的 ? ? ? ?[self willChangeValueForKey:@"header"]; // KVO ? ? ? ?objc_setAssociatedObject(self, &MJRefreshHeaderKey, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? header, OBJC_ASSOCIATION_ASSIGN); ? ? ? ?[self didChangeValueForKey:@"header"]; // KVO ? ?}}

通過上面的代碼挠进,把header添加到了UIScrollView的subviews里相艇,并保留了一個引用城看。但是這個header的frame還沒有確定,也沒有任何行為

設(shè)置header的位置和偵聽行為

由于上面執(zhí)行了addSubview肮之,接下來就會進入header的生命周期方法willMoveToSuperview,這個方法是在公共的基類MJRefreshComponent里實現(xiàn)的卜录。因為這是基礎(chǔ)的行為戈擒,所以寫在公共的基類里,所有的子類都能共享:

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

這里關(guān)鍵是設(shè)置了alwaysBounceVertical筐高,這樣才能確保UIScrollView可以下拉,否則需要處理contentSize才能拉得動丑瞧,就麻煩了很多柑土。此外這里令header也持有UIScrollView的引用,后續(xù)可以從上面取到各種屬性

然后是添加監(jiān)聽的方法addObservers嗦篱,這里主要是用了KVO的技巧:

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

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ ? ?// 遇到這些情況就直接返回 ? ?if (!self.userInteractionEnabled || self.hidden) return; ? ?if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) { ? ? ? ?[self scrollViewContentOffsetDidChange:change]; ? ?} else if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) { ? ? ? ?[self scrollViewContentSizeDidChange:change]; ? ?} else if ([keyPath isEqualToString:MJRefreshKeyPathContentInset]) { ? ? ? ?[self scrollViewContentInsetDidChange:change]; ? ?} else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) { ? ? ? ?[self scrollViewPanStateDidChange:change]; ? ?}}

這里偵聽了3個key的變化冰单,UIScrollView的contentOffset和contentSize,以及滑動手勢的狀態(tài)灸促。然后在每個value發(fā)生變化的時候诫欠,調(diào)用幾個didChange方法。這些didChange方法都是hook浴栽,是第二個擴展點荒叼,實際上都是由子類來實現(xiàn)的

接下來會進入生命周期方法layoutSubviews:

- (void)layoutSubviews{ ? ?[super layoutSubviews]; ? ?[self placeSubviews];}

這里的placeSubviews就是header應(yīng)該怎么擺,是第三個擴展點典鸡,把header的origin.y設(shè)置成負(fù)值被廓,就是在MJRefreshHeader的這個方法里實現(xiàn)的:

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

每個子類的placeSubviews方法嫁乘,都應(yīng)該先調(diào)用父類的這個方法

通過上述的代碼昆婿,確定了下拉刷新控件的位置,以及其中每個subview的位置蜓斧。并且偵聽了UIScrollView的contentOffset和contentSize變化

下拉時的實際行為

下拉會導(dǎo)致contentOffset變化仓蛆,由于前面已經(jīng)添加了KVO偵聽,所以會執(zhí)行scrollViewContentOffsetDidChange方法:

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{ ? ?[super scrollViewContentOffsetDidChange:change]; ? ?// 在刷新的refreshing狀態(tài) ? ?if (self.state == MJRefreshStateRefreshing) { ? ? ? ?// sectionheader停留解決 ? ? ? ?return; ? ?} ? ?// 跳轉(zhuǎn)到下一個控制器時挎春,contentInset可能會變 ? ?_scrollViewOriginalInset = self.scrollView.contentInset; ? ?// 當(dāng)前的contentOffset ? ?CGFloat offsetY = self.scrollView.mj_offsetY; ? ?// 頭部控件剛好出現(xiàn)的offsetY ? ?CGFloat happenOffsetY = - self.scrollViewOriginalInset.top; ? ?// 如果是向上滾動到看不見頭部控件看疙,直接返回 ? ?if (offsetY >= happenOffsetY) return; ? ?// 普通 和 即將刷新 的臨界點 ? ?CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h; ? ?CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h; ? ?if (self.scrollView.isDragging) { // 如果正在拖拽 ? ? ? ?self.pullingPercent = pullingPercent; ? ? ? ?if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) { ? ? ? ? ? ?// 轉(zhuǎn)為即將刷新狀態(tài) ? ? ? ? ? ?self.state = MJRefreshStatePulling; ? ? ? ?} else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) { ? ? ? ? ? ?// 轉(zhuǎn)為普通狀態(tài) ? ? ? ? ? ?self.state = MJRefreshStateIdle; ? ? ? ?} ? ?} else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開 ? ? ? ?// 開始刷新 ? ? ? ?[self beginRefreshing]; ? ?} else if (pullingPercent < 1) { ? ? ? ?self.pullingPercent = pullingPercent; ? ?}}

這段代碼比較長,主要是判斷offset變化是否達(dá)到了臨界值直奋,以及當(dāng)前的手勢能庆,切換header的state狀態(tài),然后根據(jù)state狀態(tài)變化脚线,驅(qū)動不同的行為:

- (void)setState:(MJRefreshState)state{ ? ?MJRefreshCheckState ? ?// 根據(jù)狀態(tài)做事情 ? ?if (state == MJRefreshStateIdle) { ? ? ? ?if (oldState != MJRefreshStateRefreshing) return; ? ? ? ?// 保存刷新時間 ? ? ? ?[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey]; ? ? ? ?[[NSUserDefaults standardUserDefaults] synchronize]; ? ? ? ?// 恢復(fù)inset和offset ? ? ? ?[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{ ? ? ? ? ? ?self.scrollView.mj_insetT -= self.mj_h; ? ? ? ? ? ?// 自動調(diào)整透明度 ? ? ? ? ? ?if (self.isAutoChangeAlpha) self.alpha = 0.0; ? ? ? ?} completion:^(BOOL finished) { ? ? ? ? ? ?self.pullingPercent = 0.0; ? ? ? ?}]; ? ?} else if (state == MJRefreshStateRefreshing) { ? ? ? ?[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{ ? ? ? ? ? ?// 增加滾動區(qū)域 ? ? ? ? ? ?CGFloat top = self.scrollViewOriginalInset.top + self.mj_h; ? ? ? ? ? ?self.scrollView.mj_insetT = top; ? ? ? ? ? ?// 設(shè)置滾動位置 ? ? ? ? ? ?self.scrollView.mj_offsetY = - top; ? ? ? ?} completion:^(BOOL finished) { ? ? ? ? ? ?[self executeRefreshingCallback]; ? ? ? ?}]; ? ?}}

setState方法是第四個擴展點搁胆,這里的MJRefreshCheckState是個宏,也調(diào)用了父類的setState的方法邮绿。下拉的時候臨時增大contentInset丰涉,令header保留在屏幕上,然后調(diào)用callback block斯碌;結(jié)束之后還原contentInset

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末一死,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子傻唾,更是在濱河造成了極大的恐慌投慈,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冠骄,死亡現(xiàn)場離奇詭異伪煤,居然都是意外死亡,警方通過查閱死者的電腦和手機凛辣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門抱既,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人扁誓,你說我怎么就攤上這事防泵。” “怎么了蝗敢?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵捷泞,是天一觀的道長。 經(jīng)常有香客問我寿谴,道長锁右,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮咏瑟,結(jié)果婚禮上拂到,老公的妹妹穿的比我還像新娘。我一直安慰自己码泞,他們只是感情好谆焊,可當(dāng)我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著浦夷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辜王。 梳的紋絲不亂的頭發(fā)上劈狐,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機與錄音呐馆,去河邊找鬼肥缔。 笑死,一個胖子當(dāng)著我的面吹牛汹来,可吹牛的內(nèi)容都是我干的续膳。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼收班,長吁一口氣:“原來是場噩夢啊……” “哼坟岔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起摔桦,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤社付,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后邻耕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸥咖,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年兄世,在試婚紗的時候發(fā)現(xiàn)自己被綠了啼辣。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡御滩,死狀恐怖鸥拧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情削解,我是刑警寧澤住涉,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站钠绍,受9級特大地震影響舆声,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一媳握、第九天 我趴在偏房一處隱蔽的房頂上張望碱屁。 院中可真熱鬧,春花似錦蛾找、人聲如沸娩脾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽柿赊。三九已至,卻和暖如春幻枉,著一層夾襖步出監(jiān)牢的瞬間碰声,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工熬甫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留胰挑,地道東北人。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓椿肩,卻偏偏與公主長得像瞻颂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子郑象,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容

  • MJRefresh是李明杰老師的作品贡这,到現(xiàn)在已經(jīng)有9800多顆star了,是一個簡單實用厂榛,功能強大的iOS下拉刷新...
    Style_mao閱讀 647評論 1 2
  • 國內(nèi)好多開發(fā)者選擇MJRefresh來實現(xiàn)下拉刷新藕坯,最近我也在讀他的源碼,在這我分享下我理解的實現(xiàn)的原理 下拉刷新...
    Rejected閱讀 6,389評論 8 28
  • 可改進部分 在 MJRefreshComponent.h 的 34 行, typedef void (^MJRef...
    在夢里失眠閱讀 510評論 0 0
  • 本文轉(zhuǎn)載自J_Knight 的MJRefresh源碼解析 MJRefresh是李明杰的作品噪沙,到現(xiàn)在已經(jīng)有9800多...
    Detective41閱讀 652評論 0 1
  • 正如源碼中注釋的一樣炼彪,這個類的作用是:負(fù)責(zé)監(jiān)控用戶下拉的狀態(tài); pragma mark - 一正歼、在.h文件中辐马,提...
    shareMind閱讀 9,548評論 0 50