MJRefresh源碼框架分析

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
 
邏輯主幹是上面的四個方法,其餘的邏輯枝葉,想本身研究的話能夠翻看源代碼。
相關文章
相關標籤/搜索