MJRefresh是一款非常優秀的重新整理控件。代碼簡潔,優雅。今天有時間對源代碼閱讀了一下。對MJRefresh的宏觀設計非常贊歎。所謂大道至簡就是這樣吧。
MJRefresh所采用的主要設計模式非常簡單,是類繼承 + 模版方法設計模式。
是以子類也主要圍繞着這幾個模版方法和繼承方法進行定制行為的。
模版方法設計模式:
由父類MJRefreshComponent定義方法接口并添加到執行步驟中,對象執行中,在特定時間一定會調用的方法。由子類在需要的時候進行自定義實作。
在MJRefreshComponent類中的重要模版方法如下:
[self prepare];//在父類initWithFrame方法調用
[self placeSubviews];//在父類layoutSubviews方法調用
類繼承:父類定義了方法的基本實作,子類在此基礎上進行持續增加,達到複雜功能。與模版方法的差別是沒有固定的執行步驟。
在MJRefreshComponent類中的重要繼承方法如下:
//狀态設定
- (void)setState:(MJRefreshState)state
//事件監聽
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
MJRefresh作為重新整理元件,核心邏輯根據ScrollView的Offset不同更新相應的狀态和資料,
根據方法名字應該是MJRefreshComponent類中的重要繼承方法:
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
下面看一下其子類MJRefreshHeader對這個方法的實作:
MJRefreshHeader是父類MJRefreshComponent的子類,其方法聲明結構如下:
紅框内是主要實作代碼應該就是這四個“覆寫父類方法”了
子類MJRefreshHeader的兩個模版方法實作如下:
- (void)prepare
{
[super prepare];
// 設定key
self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
// 設定高度
self.mj_h = MJRefreshHeaderHeight;
}
- (void)placeSubviews
{
[super placeSubviews];
// 設定y值(當自己的高度發生改變了,肯定要重新調整Y值,是以放到placeSubviews方法中設定y值)
self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}
方法prepare:設定屬性值
方法placeSubviews:更新UI布局
子類填充後,父類按照約定的步驟時機執行。over!
子類MJRefreshHeader的覆寫方法實作如下:
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在重新整理的refreshing狀态
if (self.state == MJRefreshStateRefreshing) {
// 暫時保留
//My:當NavigationBar從一個頁面滑出時,可能被移除頁面,其window為nil
if (self.window == nil) return;
// sectionheader停留解決
//My:當scrollView向下偏移的距離超過它的contentInset的上間隔時,取距離大的
CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
//My:當這個距離超過了(重新整理控件的高度 + 它的contentInset的上間隔)時,取它們的和值
insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
//My:将這個合理的最大值,設定到它的contentInset的上間隔上。
self.scrollView.mj_insetT = insetT;
//My:實際露出的重新整理空間高
self.insetTDelta = _scrollViewOriginalInset.top - insetT;
return;
}
// 跳轉到下一個控制器時,contentInset可能會變
_scrollViewOriginalInset = self.scrollView.mj_inset;
// 目前的contentOffset
CGFloat offsetY = self.scrollView.mj_offsetY;
// 頭部控件剛好出現的offsetY
CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
// 如果是向上滾動到看不見頭部控件,直接傳回
// >= -> >
if (offsetY > happenOffsetY) return;
// 普通 和 即将重新整理 的臨界點
//My:下拉距離正好是(重新整理控件高度+contentInset的上間隔)
CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
//My:露出的高度/總高度
CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
if (self.scrollView.isDragging) { // 如果正在拖拽
self.pullingPercent = pullingPercent;
if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
//My:下拉度超過臨界值
// 轉為即将重新整理狀态
self.state = MJRefreshStatePulling;
} else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
//My:下拉度小于臨界值
// 轉為普通狀态
self.state = MJRefreshStateIdle;
}
} else if (self.state == MJRefreshStatePulling) {
// 即将重新整理 && 手松開
// 開始重新整理
[self beginRefreshing];
} else if (pullingPercent < 1) {
self.pullingPercent = pullingPercent;
}
}
該方法會随着ScrollView的滾動,其Offset會不斷更新,此方法不不斷被觸發。
操作步驟大概思路是:
1.如果目前處于重新整理狀态,offset的改變時,設定scrollView的offset為(重新整理控件的高度 + 它的contentInset的上間隔)。
2.否則的話,如果處于拖拽時,根據拖拽距離和目前控件狀态,更新下一步控件的狀态。
較長的描述見上面的注釋。
帶有NavigationBar的UIScrollView,預設它的offset = {0, -64}; 預設它的contentInset = {64,0,0,0}
内容展示部分剛好在NavigationBar的下面
子類MJRefreshHeader的狀态設定後,會調用如下方法,重新整理控件的UI:
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根據狀态做事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;
// 儲存重新整理時間
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
[[NSUserDefaults standardUserDefaults] synchronize];
// 恢複inset和offset
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.scrollView.mj_insetT += self.insetTDelta;
// 自動調整透明度
if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
} completion:^(BOOL finished) {
self.pullingPercent = 0.0;
if (self.endRefreshingCompletionBlock) {
self.endRefreshingCompletionBlock();
}
}];
} else if (state == MJRefreshStateRefreshing) {
MJRefreshDispatchAsyncOnMainQueue({
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
if (self.scrollView.panGestureRecognizer.state != UIGestureRecognizerStateCancelled) {
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
// 增加滾動區域top
self.scrollView.mj_insetT = top;
// 設定滾動位置
CGPoint offset = self.scrollView.contentOffset;
offset.y = -top;
[self.scrollView setContentOffset:offset animated:NO];
}
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
})
}
}
宏MJRefreshCheckState:檢查舊狀态與新狀态是否一緻,一緻的話就傳回。
從重新整理轉普通狀态時:
儲存重新整理時間,調整菊花透明度,移動offset
轉換成重新整理狀态時:
設定contentInset.top,設定offset
邏輯主幹是上面的四個方法,其他的邏輯枝葉,想自己研究的話可以翻看源代碼。