MJRefresh原始碼剖析與學習

Dwyane_Coding發表於2018-02-08

原始碼剖析學習系列:(不斷更新)

1、FBKVOController原始碼剖析與學習
2、MJRefresh原始碼剖析與學習
3、YYImage原始碼剖析與學習


MJRefresh是李明傑大神的開源框架,這是一款十分優雅的重新整理元件庫,這開源元件無論從程式碼風格,可用性,易讀性還是相容性來講都十分優秀。本文就最新MJRefresh版本來講解。耐心看下去,本文和純解讀原始碼的文章不同。本文碼字幾天,如果對您有幫助,給個鼓勵,謝謝大家!

MJRefresh

基本結構

一、MJRefreshComponent

1.匯入檔案
#import <UIKit/UIKit.h>
#import "MJRefreshConst.h"
#import "UIView+MJExtension.h"
#import "UIScrollView+MJExtension.h"
#import "UIScrollView+MJRefresh.h"
#import "NSBundle+MJRefresh.h"
複製程式碼

匯入檔案功能

2.狀態列舉
/** 重新整理控制元件的狀態 */
typedef NS_ENUM(NSInteger, MJRefreshState) {
    /** 普通閒置狀態 */
    MJRefreshStateIdle = 1,
    /** 鬆開就可以進行重新整理的狀態 */
    MJRefreshStatePulling,
    /** 正在重新整理中的狀態 */
    MJRefreshStateRefreshing,
    /** 即將重新整理的狀態 */
    MJRefreshStateWillRefresh,
    /** 所有資料載入完畢,沒有更多的資料了 */
    MJRefreshStateNoMoreData
};
複製程式碼
3、重新整理回撥
#pragma mark - 重新整理回撥
/** 正在重新整理的回撥 */
@property (copy, nonatomic) MJRefreshComponentRefreshingBlock refreshingBlock;
/** 設定回撥物件和回撥方法 */
- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action;

/** 回撥物件 */
@property (weak, nonatomic) id refreshingTarget;
/** 回撥方法 */
@property (assign, nonatomic) SEL refreshingAction;
/** 觸發回撥(交給子類去呼叫) */
- (void)executeRefreshingCallback;
複製程式碼
4、重新整理狀態控制
#pragma mark - 重新整理狀態控制
/** 進入重新整理狀態 */
- (void)beginRefreshing;
- (void)beginRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 開始重新整理後的回撥(進入重新整理狀態後的回撥) */
@property (copy, nonatomic) MJRefreshComponentbeginRefreshingCompletionBlock beginRefreshingCompletionBlock;
/** 結束重新整理的回撥 */
@property (copy, nonatomic) MJRefreshComponentEndRefreshingCompletionBlock endRefreshingCompletionBlock;
/** 結束重新整理狀態 */
- (void)endRefreshing;
- (void)endRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 是否正在重新整理 */
@property (assign, nonatomic, readonly, getter=isRefreshing) BOOL refreshing;
//- (BOOL)isRefreshing;
/** 重新整理狀態 一般交給子類內部實現 */
@property (assign, nonatomic) MJRefreshState state;
複製程式碼

具體方法分析:

#pragma mark 進入重新整理狀態
- (void)beginRefreshing
{
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
        self.alpha = 1.0;
    }];
    self.pullingPercent = 1.0;
    // 只要正在重新整理,就完全顯示
    if (self.window) {
        self.state = MJRefreshStateRefreshing;
    } else {
        // 預防正在重新整理中時,呼叫本方法使得header inset回置失敗
        if (self.state != MJRefreshStateRefreshing) {
            self.state = MJRefreshStateWillRefresh;
            // 重新整理(預防從另一個控制器回到這個控制器的情況,回來要重新重新整理一下)
            [self setNeedsDisplay];
        }
    }
}
複製程式碼

上面做了一個動畫效果,多加了一個willRefresh的狀態,我的理解是為了防止self.window為空的時候,突然重新整理崩潰(從另一個頁面返回的時候),所以需要一個狀態來過渡。

設定state會呼叫setNeedsLayout方法;如果self.window為空,把狀態改成即將重新整理,並呼叫setNeedsDisplay

  • 首先UIViewsetNeedsDisplaysetNeedsLayout方法都是非同步執行的。而setNeedsDisplay會呼叫自動呼叫drawRect方法,這樣可以拿到 UIGraphicsGetCurrentContext,就可以繪製了,而setNeedsLayout會預設呼叫layoutSubViews,就可以處理子檢視中的一些資料。
  • 綜上所訴,setNeedsDisplay方便繪圖,而layoutSubViews方便出來資料。
//結束重新整理
- (void)endRefreshing
{
    dispatch_async(dispatch_get_main_queue(), ^{
        self.state = MJRefreshStateIdle;
    });
}

複製程式碼

在主執行緒結束重新整理,把重新整理狀態改為普通閒置狀態

5、KVO監聽
#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];
}

- (void)removeObservers
{
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentOffset];
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentSize];
    [self.pan removeObserver:self forKeyPath:MJRefreshKeyPathPanState];
    self.pan = nil;
}

- (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];
    }
}

複製程式碼

監聽ContentOffsetContentSize、手勢的State

6、回撥
#pragma mark - 內部方法
- (void)executeRefreshingCallback
{
    dispatch_async(dispatch_get_main_queue(), ^{
        if (self.refreshingBlock) {
            self.refreshingBlock();
        }
        if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
            MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
        }
        if (self.beginRefreshingCompletionBlock) {
            self.beginRefreshingCompletionBlock();
        }
    });
}
複製程式碼

MJRefreshMsgSend是時執行時objc_msgSend,第一個引數代表接收者,第二個引數代表選擇子(SEL是選擇子的型別),後續引數就是訊息中的那些引數,其順序不變。選擇子指的就是方法的名字。

二、MJRefreshHeader

1、初始化(構造方法)
#pragma mark - 構造方法
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
    MJRefreshHeader *cmp = [[self alloc] init];
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    MJRefreshHeader *cmp = [[self alloc] init];
    [cmp setRefreshingTarget:target refreshingAction:action];
    return cmp;
}

複製程式碼
2、覆蓋父類方法
- (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。

3、滾動時偏移值變化以及狀態的改變
//當scrollView的contentOffset發生改變的時候呼叫
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    // 在重新整理的refreshing狀態
    if (self.state == MJRefreshStateRefreshing) {
        // 暫時保留
        if (self.window == nil) return;
        
        // sectionheader停留解決
        //重新整理的時候:偏移量(self.scrollView.mj_offsetY) = 狀態列 + 導航欄 + header的高度(54+64=118 iphoneX則為54+88=142)
        //內邊距高度(_scrollViewOriginalInset.top)= 狀態列 + 導航欄 = 64
        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;
    }
    
    // 跳轉到下一個控制器時,contentInset可能會變
     _scrollViewOriginalInset = self.scrollView.mj_inset;
    
    // 當前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 頭部控制元件剛好出現的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 如果是向上滾動到看不見頭部控制元件,直接返回
    // >= -> >
    if (offsetY > happenOffsetY) return;
    
    // 普通閒置 即將重新整理 的臨界點
    //個人覺得normal2pullingOffsetY應該是頭部完全出來時的Y軸偏移值
    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;//手鬆開後,預設狀態時,恢復self.pullingPercent
    }
}
複製程式碼

狀態切換的因素有兩個:一個是下拉的距離是否超過臨界值,另一個是 手指是否離開螢幕。

手指還貼在螢幕的時候是不能進行重新整理的。即使在下拉的距離超過了臨界距離(狀態列 + 導航欄 + header高度),如果手指沒有離開螢幕,那麼也不能馬上進行重新整理,而是將狀態切換為:可以重新整理。一旦手指離開了螢幕,馬上將狀態切換為正在重新整理。

普通閒置與即將重新整理的分界點,看下圖,一目瞭然

MJRefresh原始碼剖析與學習

4、改變狀態時的相應操作(setter方法)
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    //MJRefreshCheckState是巨集,其實也就是下面語句,為了檢測狀態是否相同,相同則return
//    MJRefreshState oldState = self.state;
//    if (state == oldState) {
//        NSLog(@"相同");
//        return;
//    }
//    [super setState:state];

    
    // 根據狀態做事情
    if (state == MJRefreshStateIdle) {
        if (oldState != MJRefreshStateRefreshing) return;
        
        // 儲存重新整理時間
        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
        // 恢復inset和offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            //此時要加上scrollView重新整理時跟普通閒置時的偏移差值(重新整理時偏移值為118或者142,self.insetTDelta值為header高度-54),恢復後self.scrollView.mj_insetT = 64(或者88)
            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) {
         dispatch_async(dispatch_get_main_queue(), ^{
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
                // 增加滾動區域top
                self.scrollView.mj_insetT = top;
                //增加滾動區域top(賦值給scrollView.inset.top)
                CGPoint offset = self.scrollView.contentOffset;
                offset.y = -top;
                [self.scrollView setContentOffset:offset animated:NO];
            } completion:^(BOOL finished) {
                //執行正在重新整理的回撥
                [self executeRefreshingCallback];
            }];
         });
    }
}

複製程式碼

注意[super setState:state]的位置,等基類的state賦值給oldState,再跟新狀態對比,對比完後,再[super setState:state]呼叫基類,從而賦值基類state

該方法主要要注意狀態在普通閒置狀態以及重新整理狀態的scrollView.contentOffset變化

三、MJRefreshStateHeader

該類是MJRefreshHeader的子類,主要用來設定顯示上一次重新整理時間的label:lastUpdatedTimeLabel和顯示重新整理狀態的label:stateLabel屬性等

MJRefresh原始碼剖析與學習

1、stateLabel初始化方法
- (void)setTitle:(NSString *)title forState:(MJRefreshState)state
{
    if (title == nil) return;
    self.stateTitles[@(state)] = title;
    self.stateLabel.text = self.stateTitles[@(self.state)];
}

#pragma mark - 覆蓋父類的方法
- (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];
}
複製程式碼

prepare初始化方法,實現本地化(不同字型),並根據不同狀態賦值給stateLabel

2、lastUpdatedLabel賦值
#pragma mark key的處理
- (void)setLastUpdatedTimeKey:(NSString *)lastUpdatedTimeKey
{
    [super setLastUpdatedTimeKey:lastUpdatedTimeKey];
    
    // 如果label隱藏了,就不用再處理
    if (self.lastUpdatedTimeLabel.hidden) return;
    
    NSDate *lastUpdatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:lastUpdatedTimeKey];
    
    // 如果有block
    //使用者定義的時間格式
    if (self.lastUpdatedTimeText) {
        self.lastUpdatedTimeLabel.text = self.lastUpdatedTimeText(lastUpdatedTime);
        return;
    }
    
    if (lastUpdatedTime) {
        // 1.獲得年月日
        NSCalendar *calendar = [self currentCalendar];
        NSUInteger unitFlags = NSCalendarUnitYear| NSCalendarUnitMonth | NSCalendarUnitDay |NSCalendarUnitHour |NSCalendarUnitMinute;
        NSDateComponents *cmp1 = [calendar components:unitFlags fromDate:lastUpdatedTime];
        NSDateComponents *cmp2 = [calendar components:unitFlags fromDate:[NSDate date]];
        
        // 2.格式化日期
        NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
        BOOL isToday = NO;
        if ([cmp1 day] == [cmp2 day]) { // 今天
            formatter.dateFormat = @" HH:mm";  //返回11:11樣式
            isToday = YES;
        } else if ([cmp1 year] == [cmp2 year]) { // 今年
            formatter.dateFormat = @"MM-dd HH:mm"; //返回02-08 11:11樣式
        } else {
            formatter.dateFormat = @"yyyy-MM-dd HH:mm"; //返回2018-02-08 11:11樣式
        }
        NSString *time = [formatter stringFromDate:lastUpdatedTime];
        
        // 3.顯示日期
        //[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText] 會返回簡體(英文、繁體)的 【最後更新:】
        //isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"" 如果上一次重新整理也是今天,則返回簡體(英文、繁體)的 【今天】,不是則返回空字串
        self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@%@",
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
                                          isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"",
                                          time];
    } else {
    //沒有獲得上次更新時間
        self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@",
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];
    }
}
複製程式碼

注意一下時間格式,本地化以及不同上次重新整理時間的lastUpdatedTimeLabel顯示 上面程式碼還給使用者自定義時間格式,沒有才使用預設,預設的格式邏輯顯示,我已在上面註釋清楚

MJRefreshNormalHeaderMJRefreshGifHeader都是MJRefreshStateHeader的子類,前者和後者的佈局一樣,不同的就是header左邊一個是菊花的樣式,另外一個是gif,詳看下圖:

MJRefresh原始碼剖析與學習
由此看來,這兩種形式的header都有相同的共性,我們在做類似的功能時,如果有幾個控制元件或者幾個類共性一樣,比如說,一個保險類(InsuranceClass),一個房地產類(RealEstateClass),他們可以有一個基類銷售類(SalesClass),SalesClass擁有銷售員工、顧客、金額、銷售日期等 保險類 和 房地產類 需要的共同屬性

四、MJRefreshNormalHeader

1、在MJRefreshStateHeader上新增了箭頭和菊花

2、佈局這兩種樣式View,且在狀態切換時更改樣式切換

1、圈圈(菊花)和箭頭的佈局
- (void)placeSubviews
{
    [super placeSubviews];
    
    // 箭頭的中心點
    CGFloat arrowCenterX = self.mj_w * 0.5;
    if (!self.stateLabel.hidden) {
        CGFloat stateWidth = self.stateLabel.mj_textWith; //狀態label文字的寬度
        CGFloat timeWidth = 0.0;
        if (!self.lastUpdatedTimeLabel.hidden) {
            timeWidth = self.lastUpdatedTimeLabel.mj_textWith; //時間label文字的寬度
        }
        CGFloat textWidth = MAX(stateWidth, timeWidth); //求出一個最寬的文字寬度
        arrowCenterX -= textWidth / 2 + self.labelLeftInset; //箭頭(菊花)中心點x還要減去(最寬的文字寬度/2 + 文字距離圈圈、箭頭的距離)
    }
    //中心點y設定為header的高度的一半
    CGFloat arrowCenterY = self.mj_h * 0.5;
    CGPoint arrowCenter = CGPointMake(arrowCenterX, arrowCenterY);
    
    // 箭頭
    if (self.arrowView.constraints.count == 0) { //箭頭沒有其他佈局約束
        self.arrowView.mj_size = self.arrowView.image.size; //箭頭大小跟提供的arrowView圖片大小一致
        self.arrowView.center = arrowCenter;
    }
        
    // 圈圈
    if (self.loadingView.constraints.count == 0) { //圈圈(菊花)沒有其他佈局約束
        self.loadingView.center = arrowCenter;
    }
    
    self.arrowView.tintColor = self.stateLabel.textColor;
}
複製程式碼

上面程式碼主要實現了圈圈(菊花)和箭頭的佈局,需要注意的是讓箭頭菊花緊跟重新整理文字或者狀態文字居中的邏輯,我已在註釋寫明

2、不同狀態下菊花和箭頭的互換
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根據狀態做事情
    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.backgroundColor = [UIColor greenColor];
                self.loadingView.alpha = 1.0; //菊花變成完全顯示 (為什麼要這樣?求大佬告訴)
                [self.loadingView stopAnimating]; //菊花停止轉動,同時會隱藏菊花(loadingView.hidesWhenStopped = YES;)
                self.arrowView.hidden = NO; //箭頭顯示
            }];
        } else { //上次狀態是拖拽或者普通閒置狀態,準備改變成普通閒置狀態 --> 把菊花停止轉動,菊花隱藏,箭頭顯示
            [self.loadingView stopAnimating];
            self.arrowView.hidden = NO;
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                self.arrowView.transform = CGAffineTransformIdentity; //在操作結束之後對箭頭設定量進行還原
            }];
        }
    } else if (state == MJRefreshStatePulling) { //拖拽狀態:菊花停止轉動,箭頭顯示
        [self.loadingView stopAnimating];
        self.arrowView.hidden = NO;
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
            self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);//(改變箭頭的方向,但是為什麼要0.000001 - M_PI?)
        }];
    } else if (state == MJRefreshStateRefreshing) { //正在重新整理狀態:菊花完全顯示並且開始轉動,箭頭隱藏
        self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動畫完畢動作沒有被執行
        [self.loadingView startAnimating];
        self.arrowView.hidden = YES;
    }
}
複製程式碼

通過不同的狀態控制菊花和箭頭的隱藏和消失,及他們的動畫效果,如箭頭的朝上朝下,和菊花的轉與不轉

四、MJRefreshGifHeader

1、載入不同狀態對應的動畫圖片 2、設定不同狀態對應的動畫時間

1、懶載入
#pragma mark - 懶載入
//gigView顯示gif
- (UIImageView *)gifView
{
    if (!_gifView) { 
        UIImageView *gifView = [[UIImageView alloc] init]; 
        [self addSubview:_gifView = gifView]; 
    } 
    return _gifView; 
}

- (NSMutableDictionary *)stateImages 
{ 
    if (!_stateImages) { 
        self.stateImages = [NSMutableDictionary dictionary]; 
    } 
    return _stateImages; 
}

- (NSMutableDictionary *)stateDurations 
{ 
    if (!_stateDurations) { 
        self.stateDurations = [NSMutableDictionary dictionary]; 
    } 
    return _stateDurations; 
}
複製程式碼
2、設定不通過狀態對應的動畫圖片以及動畫時間
#pragma mark - 公共方法
- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state 
{ 
    if (images == nil) return; 
    
    self.stateImages[@(state)] = images; 
    self.stateDurations[@(state)] = @(duration); 
    
    /* 根據圖片設定控制元件的高度 */ 
    UIImage *image = [images firstObject]; 
    if (image.size.height > self.mj_h) { 
        self.mj_h = image.size.height; 
    } 
}

- (void)setImages:(NSArray *)images forState:(MJRefreshState)state 
{ 
    [self setImages:images duration:images.count * 0.1 forState:state]; 
}
複製程式碼
3、實現圖片的切換和gifView佈局
#pragma mark - 實現父類的方法
- (void)prepare
{
    [super prepare];
    
    // 初始化間距
    self.labelLeftInset = 20;
}

//根據拖拽進度設定透明度
- (void)setPullingPercent:(CGFloat)pullingPercent
{
    [super setPullingPercent:pullingPercent];
    NSArray *images = self.stateImages[@(MJRefreshStateIdle)]; //選擇閒置狀態下的圖片組
    if (self.state != MJRefreshStateIdle || images.count == 0) return; //狀態不是閒置或者圖片為空,則直接返回
    // 停止動畫
    [self.gifView stopAnimating];
    // 設定當前需要顯示的圖片
    NSUInteger index =  images.count * pullingPercent;
    if (index >= images.count) index = images.count - 1;
    self.gifView.image = images[index];
}

- (void)placeSubviews
{
    [super placeSubviews];
    
    if (self.gifView.constraints.count) return; //gifView沒有約束,直接返回
    
    self.gifView.frame = self.bounds;
    if (self.stateLabel.hidden && self.lastUpdatedTimeLabel.hidden) { //上次重新整理時間和狀態文字都隱藏了,圖片內容就居ifView中間顯示
        self.gifView.contentMode = UIViewContentModeCenter;
    } else { //圖片居gifView右邊顯示
        self.gifView.contentMode = UIViewContentModeRight;
        
        //下面程式碼同樣也是為了讓gifView緊挨著文字居中顯示。算出最長的文字,通過減去文字的一般寬度,調整gifView的x值,跟NormalHeader的方法一樣,詳細請看normalHeader
        CGFloat stateWidth = self.stateLabel.mj_textWith;
        CGFloat timeWidth = 0.0;
        if (!self.lastUpdatedTimeLabel.hidden) {
            timeWidth = self.lastUpdatedTimeLabel.mj_textWith;
        }
        CGFloat textWidth = MAX(stateWidth, timeWidth);
        self.gifView.mj_w = self.mj_w * 0.5 - textWidth * 0.5 - self.labelLeftInset;
    }
}

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根據狀態做事情
    if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) { //狀態變為拖拽或者正在重新整理,獲取各自狀態該顯示的圖片組
        NSArray *images = self.stateImages[@(state)];
        if (images.count == 0) return;
        
        [self.gifView stopAnimating];
        if (images.count == 1) { // 單張圖片
            self.gifView.image = [images lastObject];
        } else { // 多張圖片
            self.gifView.animationImages = images;
            self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
            [self.gifView startAnimating];
        }
    } else if (state == MJRefreshStateIdle) {
        [self.gifView stopAnimating];
    }
}
複製程式碼

到此,我對MJRefreshHeader那一塊的原始碼已經讀完,剩下MJRefreshFooter,但由於實現邏輯基本一致,故在此不再詳說。遲點,發現MJRefreshFooter有其他特殊之處,我會更新此文,謝謝大家!

學習

1、巧用Model

我們可能見到一些開發者會在didSelectRowAtIndexPath協議方法裡面這樣寫

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    MJExample *exam = self.examples[indexPath.section];
    UIViewController *vc = [[exam.vcClass alloc] init];
    vc.title = exam.titles[indexPath.row];
    [vc setValue:exam.methods[indexPath.row] forKeyPath:@"method"];
    [self.navigationController pushViewController:vc animated:YES];
    if (indexPath.row == 0) {
        UIViewController *test1 = [UIViewController new];
        test1.title = @"test1";
        [self.navigationController pushViewController:test1 animated:YES];
    }else if (indexPath.row == 1) {
        UIViewController *test2 = [UIViewController new];
        test2.title = @"test2";
        [self.navigationController pushViewController:test2 animated:YES];
    }else if (indexPath.row == 2) {
        UIViewController *test3 = [UIViewController new];
        test3.title = @"test3";
        [self.navigationController pushViewController:test3 animated:YES];
    }else {
        ;
    }
}
複製程式碼

這樣會造成didSelectRowAtIndexPath方法過於臃腫,且重複程式碼過多,太多if else 或者 switch,我們可以用Model很好的解決這個問題,程式碼如下:

- (NSArray *)examples
{
    if (!_examples) {
        MJExample *exam0 = [[MJExample alloc] init];
        exam0.header = MJExample00;
        exam0.vcClass = [MJTableViewController class];
        exam0.titles = @[@"預設", @"動畫圖片", @"隱藏時間", @"隱藏狀態和時間", @"自定義文字", @"自定義重新整理控制元件"];
        exam0.methods = @[@"example01", @"example02", @"example03", @"example04", @"example05", @"example06"];
        
        MJExample *exam1 = [[MJExample alloc] init];
        exam1.header = MJExample10;
        exam1.vcClass = [MJTableViewController class];
        exam1.titles = @[@"預設", @"動畫圖片", @"隱藏重新整理狀態的文字", @"全部載入完畢", @"禁止自動載入", @"自定義文字", @"載入後隱藏", @"自動回彈的上拉01", @"自動回彈的上拉02", @"自定義重新整理控制元件(自動重新整理)", @"自定義重新整理控制元件(自動回彈)"];
        exam1.methods = @[@"example11", @"example12", @"example13", @"example14", @"example15", @"example16", @"example17", @"example18", @"example19", @"example20", @"example21"];
        
        MJExample *exam2 = [[MJExample alloc] init];
        exam2.header = MJExample20;
        exam2.vcClass = [MJCollectionViewController class];
        exam2.titles = @[@"上下拉重新整理"];
        exam2.methods = @[@"example21"];
        
        MJExample *exam3 = [[MJExample alloc] init];
        exam3.header = MJExample30;
        exam3.vcClass = [MJWebViewViewController class];
        exam3.titles = @[@"下拉重新整理"];
        exam3.methods = @[@"example31"];
        
        self.examples = @[exam0, exam1, exam2, exam3];
    }
    return _examples;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    MJExample *exam = self.examples[indexPath.section];
    UIViewController *vc = [[exam.vcClass alloc] init];
    vc.title = exam.titles[indexPath.row];
    [vc setValue:exam.methods[indexPath.row] forKeyPath:@"method"];
    [self.navigationController pushViewController:vc animated:YES];
}
複製程式碼
2、跳轉巧用

ViewController.h

- (IBAction)tappdeBtn:(id)sender {
    UIViewController *vc = [[BViewController alloc] init];
    vc.title = @"example01";
    [vc setValue:@"example01" forKeyPath:@"method"];
    [self.navigationController pushViewController:vc animated:YES];
    
}
複製程式碼

上面是跳轉方法,請留意[vc setValue:@"example01" forKeyPath:@"method"];這句程式碼,下面會詳解

BViewController.h

#import "BViewController.h"
#import "UIViewController+Example.h"

#define MJPerformSelectorLeakWarning(Stuff) \
do { \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
Stuff; \
_Pragma("clang diagnostic pop") \
} while (0)

@implementation BViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    MJPerformSelectorLeakWarning(
                                 [self performSelector:NSSelectorFromString(self.method) withObject:nil];
                                 );
}

- (void)example01
{
    NSLog(@"進入此方法");
}
複製程式碼

結果:

MJRefresh原始碼剖析與學習

1、由上可以看到[self performSelector:NSSelectorFromString(self.method) withObject:nil];沒有指明方法名,仍可以呼叫- (void)example01(),這是運用了runtime的黑魔法,定義了UIViewController+Example分類方法,runtime的使用可以看我之前的文章-->iOS進階之runtime作用

2、MJPerformSelectorLeakWarning( );如果selector是在執行時才確定的,performSelector時,若先把selector儲存起來,等到某事件發生後再呼叫,相當於在動態繫結之上再使用動態繫結,不過這是編譯器不知道要執行的selector是什麼,因為這必須到了執行時才能確定,使用這種特性的代價是,如果在ARC下編譯程式碼,編譯器會發生警告,可用#pragma clang diagnostic ignored "-Warc-performSelector-leaks"忽略警告

#import <UIKit/UIKit.h>

@interface UIViewController (Example)
@property (copy, nonatomic) NSString *method;
@end

----------------------------

#import "UIViewController+Example.h"
#import <objc/runtime.h>

@implementation UIViewController (Example)

static char MethodKey;
- (void)setMethod:(NSString *)method
{
    objc_setAssociatedObject(self, &MethodKey, method, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)method
{
    return objc_getAssociatedObject(self, &MethodKey);
}
複製程式碼

這是runtime中為分類新增屬性的經典用法,把上面跳轉方法中的[vc setValue:@"example01" forKeyPath:@"method"];賦值的example01 利用runtime關聯,這樣分類中的method屬性值就為example01

解析一下 static char

比如有這樣一個函式
exp()
{
char a[] = "Hello!" ;
static char b[] = "Hello!" ;
}
複製程式碼

當呼叫這個函式完後,a[]就不存在了,而b[]依然存在,並且值為hello;

參考:

performSelector系列方法編譯器警告-Warc-performSelector-leaks

#pragma clang diagnostic ignored 用法

UIView常用的setNeedsDisplay和setNeedsLayout

相關文章