MJRefresh源碼分析 下拉刷新

MJRefresh是李明杰老師的一個開源項目,GitHub目前已經(jīng)有10000多star,GitHub地址是MJRefresh
下面我們一起來分析下MJRefresh框架的實現(xiàn)過程。

  • MJRefresh中類與類之間的聯(lián)系


    mjrefresh.png
  • 從我們使用MJRefresh框架的調(diào)用代碼分析
    eg:
 self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
   // 屬性中的回調(diào)
    }];
 [self.tableView.mj_header beginRefreshing];

上面的代碼會調(diào)用MJRefreshNormalHeader父類MJRefreshStateHeader的父類MJRefreshHeader的方法:

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
  // 實例化MJRefreshHeader的對象
    MJRefreshHeader *cmp = [[self alloc] init];
    // refreshingBlock 父類的屬性,把refreshingBlock賦值cmp.refreshingBlock屬性
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}

上面的headerWithRefreshingBlock:refreshingBlock;方法實例化一個一個對象cmp,會觸發(fā)MJRefreshHeader父類中的- (instancetype)initWithFrame:(CGRect)frame的方法。

#pragma mark - 初始化方法
- (instancetype)initWithFrame:(CGRect)frame
{
  // 注意,此時的self 是 MJRefreshNormalHeader的對象,為什么是 MJRefreshNormalHeader的對象,設(shè)計到繼承的知識點,可以具體參考繼承,這里就不過多的說明
    if (self = [super initWithFrame:frame]) {
        //  調(diào)用 MJRefreshNormalHeader 中prepare方法
        [self prepare];
         // 默認(rèn)是普通狀態(tài),調(diào)用MJRefreshNormalHeadersetState方法
        self.state = MJRefreshStateIdle;
    }
    return self;
}

我們回到MJRefreshNormalHeader類中的prepare方法,方法具體實現(xiàn)如下

#pragma mark - 重寫父類的方法
- (void)prepare
{
    // 調(diào)用父類的 prepare 父類 是 MJRefreshStateHeader
    [super prepare];
    // 設(shè)置菊花樣式
    self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
}

此時又會去調(diào)用MJRefreshNormalHeader父類MJRefreshStateHeader中的prepare的方法

- (void)prepare
{
    [super prepare];
    // 初始化間距  文字距離圈圈、箭頭的距離
    self.labelLeftInset = MJRefreshLabelLeftInset;
    // 初始化文字 國際化,中文,英文,繁體,
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing];
}

然后又會去調(diào)用父類中的prepare的方法,直到MJRefreshComponent類中的prepare的方法執(zhí)行完畢。關(guān)于prepare方法,里面都是做一些初始化和frame的設(shè)置,比較簡單,就不具體分析了。

再回到最開始的方法

self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
  // 屬性中的回調(diào)
   }];

MJRefreshNormalHeader視圖賦值給mj_headermj_headerUIScrollView+MJRefresh類中的屬性,要給分類添加屬性,就要用到runtime機制,具體代碼如下:

#pragma mark - header
static const char MJRefreshHeaderKey = '\0';
- (void)setMj_header:(MJRefreshHeader *)mj_header
{
    if (mj_header != self.mj_header) {
        // 刪除舊的,添加新的
        [self.mj_header removeFromSuperview];
        // A insertSubView B AtIndex:2 是將B插入到A的子視圖index為2的位置(最底下是0)
        // eg [self addsuview: mj_header];
        [self insertSubview:mj_header atIndex:0];
        // 手動kvo
        [self willChangeValueForKey:@"mj_header"]; // KVO
        // 給分類中的屬性添加一個set方法....
        // 分類能添加屬性。但是不會自己生成getter和setter方法
        objc_setAssociatedObject(self, &MJRefreshHeaderKey,
                                 mj_header, OBJC_ASSOCIATION_ASSIGN);
        // 手動kvo
        [self didChangeValueForKey:@"mj_header"]; // KVO
    }
}
// get方法
- (MJRefreshHeader *)mj_header
{
    return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}

setMj_header的方法中,監(jiān)聽屬性用了iOS 的設(shè)計模式 KVO

 [self willChangeValueForKey:@"mj_header"]; // KVO
 [self didChangeValueForKey:@"mj_header"]; // KVO

為什么要用 willChangeValueForKeydidChangeValueForKey方法監(jiān)聽分類的中的屬性,而不是- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;具體可以參考KVO的在分類中的用法

[self insertSubview:mj_header atIndex:0]; A insertSubView B AtIndex:0是將B插入到A的子視圖index為0的位置。

1、這句代碼會觸發(fā)MJRefreshComponent類中的- (void)willMoveToSuperview:(nullable UIView *)newSuperview;此方法什么時候被調(diào)用?經(jīng)過查資料得知:當(dāng)視圖即將加入父視圖時或者當(dāng)視圖即將從父視圖移除時調(diào)用,具體我們分析下此方法

// newSuperview 就是父視圖 這里值得 uiscrollerView
- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    // 如果不是UIScrollView,不做任何事情
    if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
    // 舊的父控件移除監(jiān)聽
    [self removeObservers];
    if (newSuperview) { // 新的父控件
        // 設(shè)置寬度
        self.mj_w = newSuperview.mj_w;
        // 設(shè)置位置
        self.mj_x = 0;
        // 記錄UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // 設(shè)置永遠(yuǎn)支持垂直彈簧效果
        _scrollView.alwaysBounceVertical = YES;
        // 記錄UIScrollView最開始的contentInset
        _scrollViewOriginalInset = _scrollView.contentInset;
       ;
        NSLog(@"contentInset:%@",NSStringFromUIEdgeInsets(_scrollView.contentInset));
        // 添加監(jiān)聽
        [self addObservers];
    }
}

此方法中的 self.mj_w = newSuperview.mj_w; self就是下拉的展示出來的view,mj_wUIView+MJExtension中的屬性,實現(xiàn)的set的方法- (void)setMj_w:(CGFloat)mj_w,具體方法實現(xiàn)如下

- (void)setMj_w:(CGFloat)mj_w
{
    CGRect frame = self.frame;
    frame.size.width = mj_w;
    self.frame = frame;
}

分析到這里,應(yīng)該明白了self.mj_w = newSuperview.mj_w;的意思了。self.mj_w = CGRectMake(original, original, newSuperview.mj_w, original);

[self addObservers];用KVO添加監(jiān)聽,給當(dāng)前的UIScrollView添加了contentOffset、contentSizepanGestureRecognizer 的監(jiān)聽

2、 [self insertSubview:mj_header atIndex:0];此方法還會觸發(fā)MJRefreshComponent 類中layoutSubviews方法,觸發(fā) layoutSubviews 有哪些操作?
找了下資料并總結(jié)下:
1、調(diào)用 addSubview 方法時會執(zhí)行該方法
2、設(shè)置并改變視圖的frame屬性時會觸發(fā)該方法
3、滑動UIScrollView及繼承與UIScrollView的控件時會觸發(fā)該方法
4、旋轉(zhuǎn)屏幕時,會觸發(fā)父視圖的layoutSubviews方法、設(shè)置并改變視圖的frame屬性時會觸發(fā)父視圖的layoutSubviews方法

OK,咱們一起看看MJRefreshComponent類中的layoutSubviews方法

- (void)layoutSubviews
{
// 此處的self依然是MJRefreshNormalHeader的對象
    [self placeSubviews];
    [super layoutSubviews];
}

MJRefreshNormalHeader類中的placeSubviews 添加了兩個視圖arrowView(箭頭視圖)、loadingView(菊花視圖)
MJRefreshStateHeader類中的placeSubviews 添加了兩個視圖stateLabel(狀態(tài)label )、lastUpdatedTimeLabel(顯示時間label)
MJRefreshHeader類中的placeSubviews 添加了設(shè)置了當(dāng)前視圖的Y坐標(biāo)
MJRefreshComponent類中的placeSubviews 沒有干啥 ?
鑒于placeSubviews方法比較簡單,都是關(guān)于界面的搭建,再次就不多多啰嗦了。

OK,分析到這里界面啥的都出來了。下面具體分析下拉的視圖如何出現(xiàn)
由于監(jiān)聽了UIScrollViewcontentOffset屬性,當(dāng)我們下拉的時候,觸發(fā)監(jiān)聽方法。監(jiān)聽方法在MJRefreshHeader類中

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    
    // 在刷新的refreshing狀態(tài)
    if (self.state == MJRefreshStateRefreshing) {
        if (self.window == nil) return;
        
        NSLog(@"%@",NSStringFromCGPoint(self.scrollView.contentOffset));
        // sectionheader停留解決
        
        //- self.scrollView.mj_offsetY:-(-54)= 54 : 刷新的時候,偏移量是不動的。偏移量 = 狀態(tài)欄 + 導(dǎo)航欄 + header的高度
        //_scrollViewOriginalInset.top:64 (狀態(tài)欄 + 導(dǎo)航欄)
        //insetT 取二者之間大的那一個
        CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
        
        insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
        self.scrollView.mj_insetT = insetT;
        
         // 記錄刷新的時候的偏移量
        self.insetTDelta = _scrollViewOriginalInset.top - insetT;
        return;
    }
    NSLog(@"scrollViewContentOffsetDidChange");
    // 跳轉(zhuǎn)到下一個控制器時,contentInset可能會變
     _scrollViewOriginalInset = self.scrollView.contentInset;
    // 當(dāng)前的contentOffset  Y
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 頭部控件剛好出現(xiàn)的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 如果是向上滾動到看不見頭部控件,直接返回
    // >= -> >
    // 解釋下: offsetY    正值 就是上滑動
    //         offsetY   負(fù)值  就是下拉
     if (offsetY > happenOffsetY) return;
    
    // 從普通 到 即將刷新 的臨界距離 normal2pullingOffsetY = -54
    CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
    
    //下拉的百分比:下拉的距離與header高度的比值
    CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
    
    if (self.scrollView.isDragging) { // 如果正在拖拽
        self.pullingPercent = pullingPercent;
        
        if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
        // 如果當(dāng)前為默認(rèn)狀態(tài) && 下拉的距離大于臨界距離(將tableview下拉得很低),則將狀態(tài)切換為可以刷新
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
            // 轉(zhuǎn)為普通狀態(tài)
            self.state = MJRefreshStateIdle;
        }
    }
    
    else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
        // 開始刷新
        [self beginRefreshing];
    }
    else if (pullingPercent < 1) {
                self.pullingPercent = pullingPercent;
    }
}

根據(jù)不同的state展示界面
MJRefreshStateHeader中的setState方法

- (void)setState:(MJRefreshState)state
{
//    MJRefreshCheckState
    // 狀態(tài)檢查
//#define MJRefreshCheckState \
    
    MJRefreshState oldState = self.state;
    if (state == oldState) return; 
    [super setState:state];
    // 設(shè)置狀態(tài)文字
    self.stateLabel.text = self.stateTitles[@(state)];
    // 重新設(shè)置key(重新顯示時間)
    self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}

MJRefreshNormalHeader中的setState方法

- (void)setState:(MJRefreshState)state
{

    MJRefreshState oldState = self.state;
    if (state == oldState) return;
    [super setState:state];
    
    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStateIdle) {
        
        if (oldState == MJRefreshStateRefreshing) {
            // 現(xiàn)在的狀態(tài)是 MJRefreshStateIdle ,上一個狀態(tài)時 MJRefreshStateRefreshing
            self.arrowView.transform = CGAffineTransformIdentity;
            
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                self.loadingView.alpha = 0.0;
            } completion:^(BOOL finished) {
                // 如果執(zhí)行完動畫發(fā)現(xiàn)不是idle狀態(tài),就直接返回,進(jìn)入其他狀態(tài)
                if (self.state != MJRefreshStateIdle) return;
                
                self.loadingView.alpha = 1.0;
                [self.loadingView stopAnimating];
                self.arrowView.hidden = NO;
            }];
        } else {
            // 當(dāng)它停止的時候,菊花視圖就會自動隱藏。
//             loadingView.hidesWhenStopped = YES;
            [self.loadingView stopAnimating];
            self.arrowView.hidden = NO;
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                self.arrowView.transform = CGAffineTransformIdentity;
            }];
        }
    } else if (state == MJRefreshStatePulling) {
        // loadingView 就是菊花的視圖
        [self.loadingView stopAnimating];
        // 箭頭視圖
        self.arrowView.hidden = NO;
        // 讓箭頭旋轉(zhuǎn)180°
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
            self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
        }];
        
    }
    
    else if (state == MJRefreshStateRefreshing) {
        self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動畫完畢動作沒有被執(zhí)行
        [self.loadingView startAnimating];
        self.arrowView.hidden = YES;
    }
}

MJRefreshStateHeader中的setState方法

- (void)setState:(MJRefreshState)state
{
//    MJRefreshCheckState
    // 狀態(tài)檢查
//#define MJRefreshCheckState \
    
    MJRefreshState oldState = self.state;
    if (state == oldState) return; 
    [super setState:state];
    // 設(shè)置狀態(tài)文字
    self.stateLabel.text = self.stateTitles[@(state)];
    // 重新設(shè)置key(重新顯示時間)
    self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}

MJRefreshNormalHeader中的setState方法

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

    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStateIdle) {
        if (oldState != MJRefreshStateRefreshing) return;
        // 當(dāng)前的狀態(tài)必須是 MJRefreshStateIdle ,上一個狀態(tài)是 MJRefreshStateRefreshing,才可以保存時間和恢復(fù)uiscrollerView的 inset和 offset
        
        // 保存刷新時間
        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        NSLog(@"MJRefreshState");
        // 恢復(fù)inset和offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            self.scrollView.mj_insetT += self.insetTDelta;
            NSLog(@"%@",NSStringFromUIEdgeInsets(self.scrollView.contentInset));
            // 自動調(diào)整透明度
            if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
        } completion:^(BOOL finished) {
            self.pullingPercent = 0.0;
              if (self.endRefreshingCompletionBlock) {
                self.endRefreshingCompletionBlock();
            }
        }];
    }
    else if (state == MJRefreshStateRefreshing) {
        // 對UI的調(diào)度,都應(yīng)該在主線程中
         dispatch_async(dispatch_get_main_queue(), ^{
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
                // 增加滾動區(qū)域top
                self.scrollView.mj_insetT = top;
                // 設(shè)置滾動位置
                [self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
            } completion:^(BOOL finished) {
                [self executeRefreshingCallback];
            }];
         });
    }
}

MJRefreshComponent中的setState方法

- (void)setState:(MJRefreshState)state
{
    _state = state;
    // 加入主隊列的目的是等setState:方法調(diào)用完畢、設(shè)置完文字后再去布局子控件
    dispatch_async(dispatch_get_main_queue(), ^{
        [self setNeedsLayout];
    });
}

關(guān)于下拉刷新,分析就到此為止,更多用法,參考MJRefreshDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容