MJRefresh源碼剖析與學習

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

一、FBKVOController源碼剖析與學習
二、MJRefresh源碼剖析與學習
三、YYImage源碼剖析與學習


MJRefresh是李明傑大神的開源框架,這是一款十分優雅的刷新組件庫,這開源組件不管從代碼風格,可用性,易讀性仍是兼容性來說都十分優秀。本文就最新MJRefresh版原本講解。耐心看下去,本文和純解讀源碼的文章不一樣。本文碼字幾天,若是對您有幫助,給個鼓勵,謝謝你們!bash

MJRefresh

基本結構

1、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
};
複製代碼
三、刷新回調
#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;
複製代碼
四、刷新狀態控制
#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;
複製代碼

具體方法分析:app

#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爲空,把狀態改爲即將刷新,並調用setNeedsDisplayiphone

  • 首先UIViewsetNeedsDisplaysetNeedsLayout方法都是異步執行的。而setNeedsDisplay會調用自動調用drawRect方法,這樣能夠拿到 UIGraphicsGetCurrentContext,就能夠繪製了,而setNeedsLayout會默認調用layoutSubViews,就能夠處理子視圖中的一些數據。
  • 綜上所訴,setNeedsDisplay方便繪圖,而layoutSubViews方便出來數據。
//結束刷新
- (void)endRefreshing
{
    dispatch_async(dispatch_get_main_queue(), ^{
        self.state = MJRefreshStateIdle;
    });
}

複製代碼

在主線程結束刷新,把刷新狀態改成普通閒置狀態異步

五、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、手勢的Stateasync

六、回調
#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是選擇子的類型),後續參數就是消息中的那些參數,其順序不變。選擇子指的就是方法的名字。ide

2、MJRefreshHeader

一、初始化(構造方法)
#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;
}

複製代碼
二、覆蓋父類方法
- (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。函數

三、滾動時偏移值變化以及狀態的改變
//當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高度),若是手指沒有離開屏幕,那麼也不能立刻進行刷新,而是將狀態切換爲:能夠刷新。一旦手指離開了屏幕,立刻將狀態切換爲正在刷新。post

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

四、改變狀態時的相應操做(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變化

3、MJRefreshStateHeader

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

一、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

二、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,詳看下圖:

由此看來,這兩種形式的 header都有相同的共性,咱們在作相似的功能時,若是有幾個控件或者幾個類共性同樣,好比說,一個保險類(InsuranceClass),一個房地產類(RealEstateClass),他們能夠有一個基類銷售類(SalesClass),SalesClass擁有銷售員工、顧客、金額、銷售日期等 保險類 和 房地產類 須要的共同屬性

4、MJRefreshNormalHeader

一、在MJRefreshStateHeader上添加了箭頭和菊花

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

一、圈圈(菊花)和箭頭的佈局
- (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;
}
複製代碼

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

二、不一樣狀態下菊花和箭頭的互換
- (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;
    }
}
複製代碼

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

4、MJRefreshGifHeader

一、加載不一樣狀態對應的動畫圖片 二、設置不一樣狀態對應的動畫時間

一、懶加載
#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; 
}
複製代碼
二、設置不經過狀態對應的動畫圖片以及動畫時間
#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]; 
}
複製代碼
三、實現圖片的切換和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有其餘特殊之處,我會更新此文,謝謝你們!

學習

一、巧用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];
}
複製代碼
二、跳轉巧用

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(@"進入此方法");
}
複製代碼

結果:

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

二、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

相關文章
相關標籤/搜索