寫(xiě)在前面
上一篇,我們利用Xtrace詳細(xì)地分析了MJRefresh在UIView生命周期的基礎(chǔ)上,做了哪些自定義修改。
本篇,將繼續(xù)分析其最重要的部分,動(dòng)態(tài)變化。
一、下拉刷新的實(shí)現(xiàn)原理
這部分,本想在第一篇介紹,但發(fā)現(xiàn)實(shí)現(xiàn)原理跟動(dòng)態(tài)實(shí)現(xiàn)這篇聯(lián)系比較緊密,所以還是放在這里寫(xiě)吧。
(一)初始狀態(tài)


通過(guò)上兩張圖,想必大家看出來(lái)了,MJRefresh的初始狀態(tài)下的布局,就是很簡(jiǎn)單的在UITableView可視View的上部附加了一個(gè)視圖。這樣當(dāng)我們下拉的時(shí)候,這個(gè)部分的視圖就會(huì)顯示出來(lái)。
(二)“松手刷新”狀態(tài)

這部分的實(shí)現(xiàn)原理也很簡(jiǎn)單,通過(guò)監(jiān)聽(tīng)TableView的origin,當(dāng)其超過(guò)一定數(shù)值的時(shí)候,就對(duì)視圖中的組件(這里是Arrow.png 和 Label)做動(dòng)畫(huà)。
當(dāng)然光判斷orgin還是不行的,像上圖那種情況,用戶可能會(huì)放棄刷新,所以還需要判斷Pan手勢(shì)的狀態(tài),這部分下文我們?cè)僭斦劇?/p>
(三)刷新?tīng)顟B(tài)

這個(gè)狀態(tài)涉及到的主要部分是TableView.contentInset屬性,通過(guò)修改ContentInset變相修改origin,實(shí)現(xiàn)Subviews的整體下移。
刷新結(jié)束之后,再將ContentInset復(fù)位。
對(duì)這部分不熟悉的童鞋,可以參考我之前的文章。
二、MJRefresh的實(shí)現(xiàn)方式
科學(xué)是共享的,技術(shù)可不是共享的。
好比汽車(chē)發(fā)動(dòng)機(jī),大家都知道原理很簡(jiǎn)單,但是為什么中國(guó)制造到現(xiàn)在還是比不過(guò)國(guó)外?很簡(jiǎn)單,科學(xué)原理就是那樣,但是技術(shù),那是西方列強(qiáng)一百多年的經(jīng)驗(yàn)沉淀,而且對(duì)中國(guó)實(shí)施技術(shù)封鎖,比不上人家也是必然的。
一不小心跑題了…………
下拉刷新的原理不難,而且也有很多第三方庫(kù)的封裝,MJRefresh目前應(yīng)該還是Star最多的組件,好在這是開(kāi)源的,可以讓我們一睹芳容。
(一)初始化
MJRefresh在初始化這部分的代碼的時(shí)候,只用到了KVO,監(jiān)聽(tīng)了ContentSIze、ContentInset和PanGesture三個(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];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// 遇到這些情況就直接返回
if (!self.userInteractionEnabled) return;
// 這個(gè)就算看不見(jiàn)也需要處理
if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
[self scrollViewContentSizeDidChange:change];
}
// 看不見(jiàn)
if (self.hidden) return;
if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
[self scrollViewContentOffsetDidChange:change];
} else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
[self scrollViewPanStateDidChange:change];
}
}
子類(lèi)通過(guò)覆蓋DidChange方法,實(shí)現(xiàn)自定義部分。
(二)運(yùn)行時(shí)總體流程

很平常的一個(gè)KVO過(guò)程:
- 通知MJRefresh,你監(jiān)聽(tīng)的屬性變化了
- MJRefresh接過(guò)通知
- 處理通知
(三)實(shí)現(xiàn)細(xì)節(jié)
這部分就是MJRefresh的核心部分了,其中有的部分,我也沒(méi)有弄得太明白,畢竟MJRefresh迭代了這么多版本,中間修復(fù)了很多BUG,這些BUG場(chǎng)景我可能都沒(méi)見(jiàn)過(guò),所以不涉及到核心邏輯部分的代碼,我就不細(xì)說(shuō)了,免得理解錯(cuò)了。
強(qiáng)勢(shì)插入
為了更好地理解,最好先回顧一下在你簡(jiǎn)單拖動(dòng)的時(shí)候,到底發(fā)生了什么:
其中SubView的SuperView為T(mén)ableView
subView.actualY = subView.frame.y - tableView.origin.y //公式一
tableView.origin.y = tableView.original.origin.y(初始值) - panGesture.location.y(touch坐標(biāo)) // 公式二
公式二代入公式一中,可以得出:
subView.actualY = tableView.subView.frame.y - tableView.original.origin.y + panGesture.location.y
其中subView.frame 與 tableView.original.origin.y 皆為常數(shù),也就是說(shuō)
subView.actualY = panGesture.location.y + const
即Subviews的實(shí)際位置,是與手指位置正相關(guān)的:
當(dāng)你手指向下運(yùn)動(dòng),即下拉時(shí),gesture.location.y 在增大,subViews的實(shí)際位置就會(huì)下移;
當(dāng)你手指向上運(yùn)動(dòng),即上滑時(shí),gesture.location.y 在減小,subViews的實(shí)際位置就會(huì)上移。
3.0 MJRefresh的核心處理代碼,在MJRefreshHeader文件中,其中主要是兩個(gè)方法:
- scrollViewContentOffsetDidChange
- setState
MJ本人正在辦教育,所以代碼部分自己也加了不少注釋,我只是在他的基礎(chǔ)上,增加了一些方便理解的注釋。
先介紹setState,是因?yàn)閛ffsetDidChange方法中會(huì)直接對(duì)state屬性進(jìn)行設(shè)置,也就是說(shuō),offsetDidChange方法的實(shí)現(xiàn)依賴于state。
3.1 setState
先上源代碼:
- (void)setState:(MJRefreshState)state
{
//第一步,判斷狀態(tài)是否有改變,沒(méi)有改變則直接返回
MJRefreshState oldState = self.state;
if (state == oldState) return;
[super setState:state];
//第二步,根據(jù)狀態(tài)做事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;
// 保存刷新時(shí)間
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
[[NSUserDefaults standardUserDefaults] synchronize];
// 恢復(fù)inset和offset
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.scrollView.mj_insetT += self.insetTDelta;
// 自動(dòng)調(diào)整透明度
if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
} completion:^(BOOL finished) {
self.pullingPercent = 0.0;
if (self.endRefreshingCompletionBlock) {
self.endRefreshingCompletionBlock();
}
}];
} else if (state == MJRefreshStateRefreshing) {
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
// 增加滾動(dòng)區(qū)域top
self.scrollView.mj_insetT = top;
// 設(shè)置滾動(dòng)位置
[self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
});
}
}

整個(gè)基類(lèi)方法中,并不對(duì)SubView進(jìn)行設(shè)置,主要是為了設(shè)置ContentInset,具體SubView的動(dòng)畫(huà)則由子類(lèi)去實(shí)現(xiàn)。
大體可以分為兩步:
1.主要確定是從什么樣的state轉(zhuǎn)換成現(xiàn)在的state的,即:what's oldstate → new state
2.根據(jù)不同的切換方式,設(shè)置特定的ContentInset的值:
- Idle→Pulling,基類(lèi)不做處理,子類(lèi)自定義實(shí)現(xiàn)
- 從Pulling→Refreshing,設(shè)定ContentInset,使RefreshView能夠懸停
- Refreshing→Pulling,不存在,因?yàn)樵贓ndRefreshing方法中,直接切換狀態(tài)為Idle
- 從Refreshing→Idle,則恢復(fù)ContentInset,使TableView回彈到正確的位置,隱藏RefreshView
3.1 scrollViewContentOffsetDidChange:
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
//調(diào)用Component的方法,Component這部分代碼什么也沒(méi)做
[super scrollViewContentOffsetDidChange:change];
// 下面的If代碼塊貌似是為了解決sectionHeader的懸停問(wèn)題而存在的,跟我們介紹原理關(guān)系不大,可以選擇性忽略
if (self.state == MJRefreshStateRefreshing) {
//如果還沒(méi)有加入View Hierarchy則直接返回
if (self.window == nil) return;
// sectionheader停留解決
//insetT = fmax(-offsetY,contentInset.top)
CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
//insetT = fmin(insetT, self.mj_h +contentInset.top)
insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
self.scrollView.mj_insetT = insetT;
self.insetTDelta = _scrollViewOriginalInset.top - insetT;
return;
}
// 跳轉(zhuǎn)到下一個(gè)控制器時(shí),contentInset可能會(huì)變
_scrollViewOriginalInset = self.scrollView.contentInset;
// 當(dāng)前的contentOffset
CGFloat offsetY = self.scrollView.mj_offsetY;
// 頭部控件剛好出現(xiàn)的offsetY
CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
// 如果是向上滾動(dòng)到看不見(jiàn)頭部控件,直接返回
// >= -> >
if (offsetY > happenOffsetY) return;
// 普通 和 即將刷新 的臨界點(diǎn)
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īng)顟B(tài)
self.state = MJRefreshStatePulling;
} else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
// 轉(zhuǎn)為普通狀態(tài)
self.state = MJRefreshStateIdle;
}
} else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開(kāi)
// 開(kāi)始刷新
[self beginRefreshing];
} else if (pullingPercent < 1) {
self.pullingPercent = pullingPercent;
}
}

其中,方法beginRefreshing的代碼如下:
- (void)beginRefreshing
{
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.alpha = 1.0;
}];
self.pullingPercent = 1.0;
// 只要正在刷新,就完全顯示
if (self.window) {
self.state = MJRefreshStateRefreshing;
} else {
// 預(yù)防正在刷新中時(shí),調(diào)用本方法使得header inset回置失敗
if (self.state != MJRefreshStateRefreshing) {
self.state = MJRefreshStateWillRefresh;
// 刷新(預(yù)防從另一個(gè)控制器回到這個(gè)控制器的情況,回來(lái)要重新刷新一下)
[self setNeedsDisplay];
}
}
}
由我們自己調(diào)用的EndRefreshing方法代碼如下:
- (void)endRefreshing
{
self.state = MJRefreshStateIdle;
}
整個(gè)方法,忽略掉解決Section懸停問(wèn)題的部分代碼,首先進(jìn)行判斷的就是PanGesture.state,確定用戶是否還在拖曳。
- 只有在用戶松手并且self.state==Pullingstate的情況下,才會(huì)進(jìn)入刷新?tīng)顟B(tài)。
- 其余情況,會(huì)根據(jù)下拉的位移量與閾值大小的比較結(jié)果,在Idle和Pulling狀態(tài)之間來(lái)回切換。
有幾個(gè)參數(shù)需要解釋一下:
happenOffsetY
該參數(shù)的值是初始化狀態(tài)下的TableView.origin.y,其主要作用是判斷MJRefreshView是否需要顯示。
參考上面的公式二
tableView.origin.y = tableView.original.origin.y(初始值) - panGesture.location.y(touch坐標(biāo))
即
tableView.contentOffset.y = happenOffsetY - panGesture.location.y
當(dāng)contentOffsetY > happenOffsetY 時(shí),說(shuō)明用戶正在下滑或者下滑之后還沒(méi)有返回到初始狀態(tài),這時(shí)候MJRefreshView是沒(méi)有顯示的,所以直接返回就好了;
當(dāng)contentOffsetY < happenOffsety 時(shí),情況正好相反,我們就需要開(kāi)始處理數(shù)據(jù)了。
normal2pullingOffsetY
- 顧名思義,該參數(shù)是判斷是否需要從Normal狀態(tài)轉(zhuǎn)換為Pulling狀態(tài)的閾值。也就是說(shuō),下拉的位移量(abs(nowOffset.y - originnal.offset.y))要不小于此參數(shù)的絕對(duì)值才能進(jìn)入Pulling狀態(tài)。
- 其實(shí)際值為MJRefreshView.height + originalContentInset.top,即完全顯示MJRefreshView所需要的高度。
比如說(shuō):
嵌入NavigationBar的話,其值為:-54 - 64 = -118
三、總結(jié)
至此,MJRefresh的實(shí)現(xiàn)細(xì)節(jié)部分,終于分析完了。
從整體上來(lái)看,MJRefresh采用了Template模式,即模板模式。
通過(guò)基類(lèi)定義整體的流程和共用方法,由子類(lèi)去延遲實(shí)現(xiàn)特定的方法,這樣可以在不破壞整個(gè)算法結(jié)構(gòu)的同時(shí),重新定義該算法的某些步驟。
不得不說(shuō),MJRefresh在基類(lèi)中定義的方法實(shí)現(xiàn),都很?chē)?yán)謹(jǐn),確實(shí)是一個(gè)優(yōu)秀的第三方庫(kù)。
所以,一般情況下,直接使用MJRefresh作為刷新控件是一個(gè)很好的選擇,當(dāng)我們有自己的需求的時(shí)候,可以很方面的繼承MJRefreshHeader,實(shí)現(xiàn)自定義的RefreshView。