MJRefresh原理分析

國內好多開發者選擇MJRefresh來實現下拉刷新,最近我也在讀他的源碼,在這我分享下我理解的實現的原理

下拉刷新的基本原理

  • 一般的下拉刷新都是用<code>contentInset</code>來實現的,如果一個tableView在導航欄的正下方,那么他的<code>contentInset.top</code>就是64,<code>contentOffset.y</code>就是-64。繼續下拉<code>contentInset.top</code>不變,<code>contentOffset.y</code>變小,上拉<code>contentOffset.y</code> 變大,直到左上角達到屏幕的左上角變為0。
  • 默認情況下,下拉一個tableView,在松手之后,會彈回初始的位置。而下拉刷新控件,就是將自己放在tableView的上方,初始y設置成負數,所以平時不會顯示出來,只有下拉的時候才會出現,放開又會彈回去。然后在loading的時候,臨時把<code>contentInset</code>增大,相當于把tableView往下擠,于是下拉刷新的控件就會顯示出來,然后刷新完成之后,再把<code>contentInset</code>改回原來的值,實現回彈的效果

關鍵代碼分析實現

  • 從創建實例的代碼開始
    [self.tableView addHeaderWithTarget:self action:@selector(loadNewData)];
 [self.tableView headerEndRefreshing];
  • 這是tagetAction方法,還有一種block方法,在方法中寫著真正的headerView初始化方法
- (void)addHeaderWithTarget:(id)target action:(SEL)action
{
    [self addHeaderWithTarget:target action:action dateKey:nil];
}

- (void)addHeaderWithTarget:(id)target action:(SEL)action dateKey:(NSString*)dateKey
{
    // 1.創建新的header
    if (!self.header) {
        MJRefreshHeaderView *header = [MJRefreshHeaderView header];
        [self addSubview:header];
        self.header = header;
    }
    
    // 2.設置目標和回調方法
    self.header.beginRefreshingTaget = target;
    self.header.beginRefreshingAction = action;
    
    // 3.設置存儲刷新時間的key
    self.header.dateKey = dateKey;
}
  • <code>MJRefreshHeaderView *header = [MJRefreshHeaderView header]; </code>, 這個方法是第一個擴展點,具體的header有哪些屬性,哪些用戶設置的樣式都是在這里設置的,但是它現在還沒有加到任何的superView商,也沒有任何行為將其掛到tableView上,
    接下來的調用<code>self.header = header;</code> ,這里運用的技巧是利用了UIScrollView+MJRefresh里的一個category,為UIScrollView增加了屬性header和footer,在set.get方法中實現,如下:
- (void)setHeader:(MJRefreshHeaderView *)header {
    [self willChangeValueForKey:@"MJRefreshHeaderViewKey"];
    objc_setAssociatedObject(self, &MJRefreshHeaderViewKey,
                             header,
                             OBJC_ASSOCIATION_ASSIGN);
    [self didChangeValueForKey:@"MJRefreshHeaderViewKey"];
}

- (MJRefreshHeaderView *)header {
    return objc_getAssociatedObject(self, &MJRefreshHeaderViewKey);
}
  • 這里用到了關聯對象的技巧(AssociatedObject),因為category通常情況下是不能直接添加實例變量的、通過上面的代碼,把header添加到了UIScrollView的subviews里,并保留了一個引用。但是這個header的frame還沒有確定,也沒有任何行為設置header的位置和偵聽行為

  • <code>[self addSubview:header];</code> 由于執行了這句代碼,接下來就會進入header的生命周期方法<code>
    willMoveToSuperview</code>,這個方法是在公共的基類<code>MJRefreshBaseView</code>里實現的,因為這是基礎的行為,所以寫在公共的基類里,所有的子類都能共享:

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    // 舊的父控件
    [self.superview removeObserver:self forKeyPath:MJRefreshContentOffset context:nil];
    
    if (newSuperview) { // 新的父控件
        [newSuperview addObserver:self forKeyPath:MJRefreshContentOffset options:NSKeyValueObservingOptionNew context:nil];
        
        // 設置寬度
        self.mj_width = newSuperview.mj_width;
        // 設置位置
        self.mj_x = 0;
        
        // 記錄UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // 記錄UIScrollView最開始的contentInset
        _scrollViewOriginalInset = _scrollView.contentInset;
    }
}
  • 接下來會進入生命周期方法layoutSubviews:
- (void)layoutSubviews
{
    [super layoutSubviews];
    
    // 1.箭頭
    CGFloat arrowX = self.mj_width * 0.5 - 100;
    self.arrowImage.center = CGPointMake(arrowX, self.mj_height * 0.5);
    
    // 2.指示器
    self.activityView.center = self.arrowImage.center;
}
  • 通過上述的代碼,確定了控件的位置,以及其中每個subview的位置。
  • 接下來就是偵聽<code>UIScrollView</code>的<code>contentOffset</code>和<code>contentSize</code>變化,關鍵代碼如下:
#pragma mark - 監聽UIScrollView的contentOffset屬性
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // 不能跟用戶交互就直接返回
    if (!self.userInteractionEnabled || self.alpha <= 0.01 || self.hidden) return;

    // 如果正在刷新,直接返回
    if (self.state == MJRefreshStateRefreshing) return;

    if ([MJRefreshContentOffset isEqualToString:keyPath]) {
        [self adjustStateWithContentOffset];
    }
}

/**
 *  調整狀態
 */
- (void)adjustStateWithContentOffset
{
    // 當前的contentOffset
    CGFloat currentOffsetY = self.scrollView.mj_contentOffsetY;
    // 頭部控件剛好出現的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 如果是向上滾動到看不見頭部控件,直接返回
    if (currentOffsetY >= happenOffsetY) return;
    
    if (self.scrollView.isDragging) {
        // 普通 和 即將刷新 的臨界點
        CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_height;
        
        if (self.state == MJRefreshStateNormal && currentOffsetY < normal2pullingOffsetY) {
            // 轉為即將刷新狀態
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && currentOffsetY >= normal2pullingOffsetY) {
            // 轉為普通狀態
            self.state = MJRefreshStateNormal;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
        // 開始刷新
        self.state = MJRefreshStateRefreshing;
    }
}
  • 接下來是設置狀態的方法,根據state狀態變化,驅動不同的行為。setState方法是第二個擴展點,這里的MJRefreshCheckState是個宏,也調用了父類的setState的方法。下拉的時候臨時增大contentInset,令header保留在屏幕上,然后調用callback block;結束之后還原contentInset,如下:
- (void)setState:(MJRefreshState)state
{
    // 0.存儲當前的contentInset
    if (self.state != MJRefreshStateRefreshing) {
        _scrollViewOriginalInset = self.scrollView.contentInset;
    }
    // 1.一樣的就直接返回(暫時不返回)
    if (self.state == state) return;
    
    // 2.舊狀態
    MJRefreshState oldState = self.state;
    
    // 3.存儲狀態
    _state = state;
    
    // 4.根據狀態執行不同的操作
    switch (state) {
  case MJRefreshStateNormal: // 普通狀態
        {
            if (oldState == MJRefreshStateRefreshing) {
                [UIView animateWithDuration:MJRefreshSlowAnimationDuration * 0.6 animations:^{
                    self.activityView.alpha = 0.0;
                } completion:^(BOOL finished) {
                    // 停止轉圈圈
                    [self.activityView stopAnimating];
                    
                    // 恢復alpha
                    self.activityView.alpha = 1.0;
                }];
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(MJRefreshSlowAnimationDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 等頭部回去
                    // 再次設置回normal
//                    _state = MJRefreshStatePulling;
//                    self.state = MJRefreshStateNormal;
                    // 顯示箭頭
                    self.arrowImage.hidden = NO;
                    
                    // 停止轉圈圈
                    [self.activityView stopAnimating];
                    
                    // 設置文字
                    [self settingLabelText];
                });
                // 直接返回
                return;
            } else {
                // 顯示箭頭
                self.arrowImage.hidden = NO;
                
                // 停止轉圈圈
                [self.activityView stopAnimating];
            }
   break;
        }
        case MJRefreshStatePulling:
            break;
  case MJRefreshStateRefreshing:
        {
            // 開始轉圈圈
   [self.activityView startAnimating];
            // 隱藏箭頭
   self.arrowImage.hidden = YES;
            
            // 回調
            if ([self.beginRefreshingTaget respondsToSelector:self.beginRefreshingAction]) {
                msgSend(msgTarget(self.beginRefreshingTaget), self.beginRefreshingAction, self);
            }
            if (self.beginRefreshingCallback) {
                self.beginRefreshingCallback();
            }
   break;
        }
        default:
            break;
 }
    
    // 5.設置文字
    [self settingLabelText];
}
  • 到此,已經將MJRefresh的原理分析的差不多了,通過分析它的原理,可以看出來下拉刷新的過程大致是:
    初始化下拉刷新控件 - 設置Frame - 控件添加監聽 - 監控contentOffset - 判斷contentOffset并做出相應回調
  • 希望能給各位想自己寫下拉刷新控件的帶來點幫助,有什么寫的不對、有問題的地方,希望各位能批評指正
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容