MJRefresh源碼解讀(一)

本篇先帶著問題來看MJRefresh,在下拉時MJRefresh是怎么使箭頭旋轉,又是如何使菊花(或其他動畫圖片)停留一段時間的呢?效果看下圖。

截圖一.gif

于是乎我對MJRefresh探究了一番,MJRefresh源碼地址,查看MJRefresh在GitHub的介紹可以得知它的主要成員如下圖:
MJRefresh集成關系.png

所有的刷新控件都是繼承于基類MJRsfreshComponent的。第一步看看MJRsfreshComponent.m文件是怎么寫的。

一.對scrollView對象添加監聽:

#pragma mark - KVO監聽
- (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];
}

可以看出對scrollView對象添加了KVO監聽,當scrollView有滑動手勢操作,contentOffset屬性值有變化時,進行一些處理。使得有下拉箭頭的變化和菊花的顯示,那么具體是怎么實現操作的呢,咱接著分析。

二.查看對contentOffset的監聽

先來了解一下什么是contentOffset:是scrollView基本的屬性。

contentOffset:即偏移量,其中分為contentOffset.y=內容的頂部和frame頂部的差值,contentOffset.x=內容的左邊和frame左邊的差值。

其實對contentOffset進行監聽就是看scrollView的內容是否有偏移的變化。

在MJRefreshComponent.m文件中實現了觀察監聽的方法。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // 遇到這些情況就直接返回
    if (!self.userInteractionEnabled) return;
    
    // 這個就算看不見也需要處理
    if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
        [self scrollViewContentSizeDidChange:change];
    }
    
    // 看不見
    if (self.hidden) return;
    if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
        [self scrollViewContentOffsetDidChange:change];
    } else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
        [self scrollViewPanStateDidChange:change];
    }
}

當contentOffset屬性有變化時,調用- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}方法。可以看到這個方法只是在基類MJRefreshComponent僅寫了空方法,但是在MJRefreshHeader.m文件中具體實現了這個方法

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    

    // 在刷新的refreshing狀態
    if (self.state == MJRefreshStateRefreshing) {

        // sectionheader停留解決
       ...
        return;
    }
    
  ...
    
    // 當前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 頭部控件剛好出現的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 如果是向上滾動到看不見頭部控件,直接返回
    // >= -> >
    if (offsetY > happenOffsetY) return;
    
    // 普通 和 即將刷新 的臨界點
    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) {
            // 轉為即將刷新狀態
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
            // 轉為普通狀態
            self.state = MJRefreshStateIdle;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
        // 開始刷新
        [self beginRefreshing];
    } else if (pullingPercent < 1) {
        self.pullingPercent = pullingPercent;
    }
}

我的理解是在- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change方法實現里,提現的是對state的狀態的判斷和改變,而對- (void)setState:(MJRefreshState)state的實現,幾個刷新空間都有自己的任務。

三.對state的改變做任務

在對MJRefreshComponent、MJRefreshHeader、MJRefreshStateHeader、MJRefreshNormalHeader四個文件查看是都有實現state的setter方法。不同的是后邊的三個子類方法里都有MJRefreshCheckState這個宏。對狀態做了判斷和調用父類的方法。

// 狀態檢查
#define MJRefreshCheckState \
MJRefreshState oldState = self.state; \
if (state == oldState) return; \
[super setState:state]; \

分別在三個子類中添加一個行打印方法的代碼,如下:

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
   
    NSLog(@"%s",__func__);//添加的打印方法
    
    // 根據狀態做事情
    if (state == MJRefreshStateIdle) {
    ...
    } else if (state == MJRefreshStateRefreshing) {
       ...
    }
}

運行demo程序,打印結果如下:

setState.png

可見這四個刷新控件會依次執行- (void)setState:(MJRefreshState)state

主要查看MJRefreshNormalHeader文件的- (void)setState:(MJRefreshState)state方法。

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    NSLog(@"%s",__func__);
    // 根據狀態做事情
    if (state == MJRefreshStateIdle) {//閑置狀態
        if (oldState == MJRefreshStateRefreshing) {//正在刷新中的狀態
            self.arrowView.transform = CGAffineTransformIdentity;
            
            [...
        } else {
            ...
        }
    } else if (state == MJRefreshStatePulling) {//松開就可以進行刷新的狀態
        [self.loadingView stopAnimating];
        self.arrowView.hidden = NO;
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
            self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
            NSLog(@"----transform");
        }];
    } else if (state == MJRefreshStateRefreshing) {//正在刷新中的狀態
        self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動畫完畢動作沒有被執行
        [self.loadingView startAnimating];
        self.arrowView.hidden = YES;
    }
}

四.解答疑問

分析到這里就可以解釋文章開頭拋出的問題了:

在下拉時MJRefresh是怎么使箭頭旋轉,又是如何使菊花(或其他動畫圖片)停留一段時間的呢?


1.由于拖拽scrollview使其contentOffset發生了變化
2.在監聽contentOffset發生變化的方法里判斷偏移量的變化
3.根據偏移量的變化來設置當前state的值,即對當前的刷新狀態進行改變
4.根據state的變化來做箭頭旋轉和菊花(或其他動畫)的展示

在第四步中,有更為詳細的處理:利用UIView的+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion來處理動畫事件。

那么菊花(或其他動畫)是如何保持一段時間然后消失的呢?

1.初始化header

在初始化tableView.mj_header可以看到:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    ...

    // 下拉刷新
    tableView.mj_header= [MJRefreshNormalHeader headerWithRefreshingBlock:^{
        // 模擬延遲加載數據,因此2秒后才調用(真實開發中,可以移除這段gcd代碼)
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 結束刷新
            [tableView.mj_header endRefreshing];
        });
    }];
    ...
}

在這里是模擬延遲加載數據,當有下拉動作并松手時時,菊花會一直顯示,知道調用[tableView.mj_header endRefreshing]

2.調用- (void)endRefreshing結束刷新事件

#pragma mark 結束刷新狀態
- (void)endRefreshing
{
    dispatch_async(dispatch_get_main_queue(), ^{
        self.state = MJRefreshStateIdle;
    });
}

是把當前的刷新狀態state直接改變為普通閑置狀態

3.state改變的處理事件

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    NSLog(@"%s",__func__);
    // 根據狀態做事情
    if (state == MJRefreshStateIdle) {
        if (oldState == MJRefreshStateRefreshing) {
            self.arrowView.transform = CGAffineTransformIdentity;
            //菊花停止并給隱藏
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                self.loadingView.alpha = 0.0;
            } completion:^(BOOL finished) {
                // 如果執行完動畫發現不是idle狀態,就直接返回,進入其他狀態
                if (self.state != MJRefreshStateIdle) return;
                
                self.loadingView.alpha = 1.0;
                [self.loadingView stopAnimating];
                self.arrowView.hidden = NO;
            }];
        } else {
          ...
    } else if (state == MJRefreshStatePulling) {
       ...
    } else if (state == MJRefreshStateRefreshing) {
        ...
    }
}

總結:可以看出MJRefresh的刷新機制是流暢和完善的,并且是連續的完成刷新事件。在幾個刷新控件的的功能實現上各有分工,使得該開源庫讀起來簡潔并且有調理,用起來也較為方便。

參考鏈接:contentSize、contentOffset和contentInset的圖解辨別

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容