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
邏輯主幹是上面的四個方法,其他的邏輯枝葉,想自己研究的話可以翻看原始碼。