MJRefresh源碼剖析與學(xué)習(xí)

建議查看原文:http://www.itdecent.cn/p/23c876f8ae39(不定時(shí)更新)

源碼剖析學(xué)習(xí)系列:(不斷更新)

1、FBKVOController源碼剖析與學(xué)習(xí)
2、MJRefresh源碼剖析與學(xué)習(xí)
3、YYImage源碼剖析與學(xué)習(xí)


MJRefresh是李明杰大神的開(kāi)源框架,這是一款十分優(yōu)雅的刷新組件庫(kù),這開(kāi)源組件無(wú)論從代碼風(fēng)格,可用性,易讀性還是兼容性來(lái)講都十分優(yōu)秀。本文就最新MJRefresh版本來(lái)講解。耐心看下去,本文和純解讀源碼的文章不同。本文碼字幾天,如果對(duì)您有幫助,給個(gè)鼓勵(lì),謝謝大家!

MJRefresh

基本結(jié)構(gòu)

一、MJRefreshComponent

1.導(dǎo)入文件
#import <UIKit/UIKit.h>
#import "MJRefreshConst.h"
#import "UIView+MJExtension.h"
#import "UIScrollView+MJExtension.h"
#import "UIScrollView+MJRefresh.h"
#import "NSBundle+MJRefresh.h"
導(dǎo)入文件功能
2.狀態(tài)枚舉
/** 刷新控件的狀態(tài) */
typedef NS_ENUM(NSInteger, MJRefreshState) {
    /** 普通閑置狀態(tài) */
    MJRefreshStateIdle = 1,
    /** 松開(kāi)就可以進(jìn)行刷新的狀態(tài) */
    MJRefreshStatePulling,
    /** 正在刷新中的狀態(tài) */
    MJRefreshStateRefreshing,
    /** 即將刷新的狀態(tài) */
    MJRefreshStateWillRefresh,
    /** 所有數(shù)據(jù)加載完畢,沒(méi)有更多的數(shù)據(jù)了 */
    MJRefreshStateNoMoreData
};
3、刷新回調(diào)
#pragma mark - 刷新回調(diào)
/** 正在刷新的回調(diào) */
@property (copy, nonatomic) MJRefreshComponentRefreshingBlock refreshingBlock;
/** 設(shè)置回調(diào)對(duì)象和回調(diào)方法 */
- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action;

/** 回調(diào)對(duì)象 */
@property (weak, nonatomic) id refreshingTarget;
/** 回調(diào)方法 */
@property (assign, nonatomic) SEL refreshingAction;
/** 觸發(fā)回調(diào)(交給子類去調(diào)用) */
- (void)executeRefreshingCallback;
4、刷新?tīng)顟B(tài)控制
#pragma mark - 刷新?tīng)顟B(tài)控制
/** 進(jìn)入刷新?tīng)顟B(tài) */
- (void)beginRefreshing;
- (void)beginRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 開(kāi)始刷新后的回調(diào)(進(jìn)入刷新?tīng)顟B(tài)后的回調(diào)) */
@property (copy, nonatomic) MJRefreshComponentbeginRefreshingCompletionBlock beginRefreshingCompletionBlock;
/** 結(jié)束刷新的回調(diào) */
@property (copy, nonatomic) MJRefreshComponentEndRefreshingCompletionBlock endRefreshingCompletionBlock;
/** 結(jié)束刷新?tīng)顟B(tài) */
- (void)endRefreshing;
- (void)endRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 是否正在刷新 */
@property (assign, nonatomic, readonly, getter=isRefreshing) BOOL refreshing;
//- (BOOL)isRefreshing;
/** 刷新?tīng)顟B(tài) 一般交給子類內(nèi)部實(shí)現(xiàn) */
@property (assign, nonatomic) MJRefreshState state;

具體方法分析:

#pragma mark 進(jìn)入刷新?tīng)顟B(tài)
- (void)beginRefreshing
{
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
        self.alpha = 1.0;
    }];
    self.pullingPercent = 1.0;
    // 只要正在刷新,就完全顯示
    if (self.window) {
        self.state = MJRefreshStateRefreshing;
    } else {
        // 預(yù)防正在刷新中時(shí),調(diào)用本方法使得header inset回置失敗
        if (self.state != MJRefreshStateRefreshing) {
            self.state = MJRefreshStateWillRefresh;
            // 刷新(預(yù)防從另一個(gè)控制器回到這個(gè)控制器的情況,回來(lái)要重新刷新一下)
            [self setNeedsDisplay];
        }
    }
}

上面做了一個(gè)動(dòng)畫(huà)效果,多加了一個(gè)willRefresh的狀態(tài),我的理解是為了防止self.window為空的時(shí)候,突然刷新崩潰(從另一個(gè)頁(yè)面返回的時(shí)候),所以需要一個(gè)狀態(tài)來(lái)過(guò)渡。

設(shè)置state會(huì)調(diào)用setNeedsLayout方法;如果self.window為空,把狀態(tài)改成即將刷新,并調(diào)用setNeedsDisplay

  • 首先UIViewsetNeedsDisplaysetNeedsLayout方法都是異步執(zhí)行的。而setNeedsDisplay會(huì)調(diào)用自動(dòng)調(diào)用drawRect方法,這樣可以拿到 UIGraphicsGetCurrentContext,就可以繪制了,而setNeedsLayout會(huì)默認(rèn)調(diào)用layoutSubViews,就可以處理子視圖中的一些數(shù)據(jù)。
  • 綜上所訴,setNeedsDisplay方便繪圖,而layoutSubViews方便出來(lái)數(shù)據(jù)。
//結(jié)束刷新
- (void)endRefreshing
{
    dispatch_async(dispatch_get_main_queue(), ^{
        self.state = MJRefreshStateIdle;
    });
}

在主線程結(jié)束刷新,把刷新?tīng)顟B(tài)改為普通閑置狀態(tài)

5、KVO監(jiān)聽(tīng)
#pragma mark - KVO監(jiān)聽(tīng)
- (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;
    
    // 這個(gè)就算看不見(jiàn)也需要處理
    if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
        [self scrollViewContentSizeDidChange:change];
    }
    
    // 看不見(jiàn)
    if (self.hidden) return;
    if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
        [self scrollViewContentOffsetDidChange:change];
    } else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
        [self scrollViewPanStateDidChange:change];
    }
}

監(jiān)聽(tīng)ContentOffsetContentSize、手勢(shì)的State

6、回調(diào)
#pragma mark - 內(nèi)部方法
- (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是時(shí)運(yùn)行時(shí)objc_msgSend,第一個(gè)參數(shù)代表接收者,第二個(gè)參數(shù)代表選擇子(SEL是選擇子的類型),后續(xù)參數(shù)就是消息中的那些參數(shù),其順序不變。選擇子指的就是方法的名字。

二、MJRefreshHeader

1、初始化(構(gòu)造方法)
#pragma mark - 構(gòu)造方法
+ (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];
    
    // 設(shè)置key
    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
    
    // 設(shè)置高度
    self.mj_h = MJRefreshHeaderHeight;
}

- (void)placeSubviews
{
    [super placeSubviews];
    
    // 設(shè)置y值(當(dāng)自己(頭部)的高度發(fā)生改變了,肯定要重新調(diào)整Y值,所以放到placeSubviews方法中設(shè)置y值)
    self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}

prepare設(shè)置一下初始化值數(shù)據(jù)。而placeSubViews更新一下UI。

3、滾動(dòng)時(shí)偏移值變化以及狀態(tài)的改變
//當(dāng)scrollView的contentOffset發(fā)生改變的時(shí)候調(diào)用
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    // 在刷新的refreshing狀態(tài)
    if (self.state == MJRefreshStateRefreshing) {
        // 暫時(shí)保留
        if (self.window == nil) return;
        
        // sectionheader停留解決
        //刷新的時(shí)候:偏移量(self.scrollView.mj_offsetY) = 狀態(tài)欄 + 導(dǎo)航欄 + header的高度(54+64=118 iphoneX則為54+88=142)
        //內(nèi)邊距高度(_scrollViewOriginalInset.top)= 狀態(tài)欄 + 導(dǎo)航欄 = 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;
    }
    
    // 跳轉(zhuǎn)到下一個(gè)控制器時(shí),contentInset可能會(huì)變
     _scrollViewOriginalInset = self.scrollView.mj_inset;
    
    // 當(dāng)前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 頭部控件剛好出現(xiàn)的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 如果是向上滾動(dòng)到看不見(jiàn)頭部控件,直接返回
    // >= -> >
    if (offsetY > happenOffsetY) return;
    
    // 普通閑置 即將刷新 的臨界點(diǎn)
    //個(gè)人覺(jué)得normal2pullingOffsetY應(yīng)該是頭部完全出來(lái)時(shí)的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) { //手指拖拽中,狀態(tài)是默認(rèn)狀態(tài)以及下拉距離(偏移值)大于臨界點(diǎn)距離
            // 轉(zhuǎn)為可以進(jìn)行刷新?tīng)顟B(tài)
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
        //手指拖拽中,狀態(tài)是默認(rèn)狀態(tài)以及下拉距離(偏移值)小于臨界點(diǎn)距離,也就是拖得比較下
            // 轉(zhuǎn)為普通狀態(tài)
            self.state = MJRefreshStateIdle;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開(kāi)
        // 開(kāi)始刷新
        [self beginRefreshing];
    } else if (pullingPercent < 1) {
        self.pullingPercent = pullingPercent;//手松開(kāi)后,默認(rèn)狀態(tài)時(shí),恢復(fù)self.pullingPercent
    }
}

狀態(tài)切換的因素有兩個(gè):一個(gè)是下拉的距離是否超過(guò)臨界值,另一個(gè)是 手指是否離開(kāi)屏幕。

手指還貼在屏幕的時(shí)候是不能進(jìn)行刷新的。即使在下拉的距離超過(guò)了臨界距離(狀態(tài)欄 + 導(dǎo)航欄 + header高度),如果手指沒(méi)有離開(kāi)屏幕,那么也不能馬上進(jìn)行刷新,而是將狀態(tài)切換為:可以刷新。一旦手指離開(kāi)了屏幕,馬上將狀態(tài)切換為正在刷新。

普通閑置與即將刷新的分界點(diǎn),看下圖,一目了然

image
4、改變狀態(tài)時(shí)的相應(yīng)操作(setter方法)
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    //MJRefreshCheckState是宏,其實(shí)也就是下面語(yǔ)句,為了檢測(cè)狀態(tài)是否相同,相同則return
//    MJRefreshState oldState = self.state;
//    if (state == oldState) {
//        NSLog(@"相同");
//        return;
//    }
//    [super setState:state];

    
    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStateIdle) {
        if (oldState != MJRefreshStateRefreshing) return;
        
        // 保存刷新時(shí)間
        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
        // 恢復(fù)inset和offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            //此時(shí)要加上scrollView刷新時(shí)跟普通閑置時(shí)的偏移差值(刷新時(shí)偏移值為118或者142,self.insetTDelta值為header高度-54),恢復(fù)后self.scrollView.mj_insetT = 64(或者88)
            self.scrollView.mj_insetT += self.insetTDelta;
            
            // 自動(dòng)調(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) {
         dispatch_async(dispatch_get_main_queue(), ^{
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
                // 增加滾動(dòng)區(qū)域top
                self.scrollView.mj_insetT = top;
                //增加滾動(dòng)區(qū)域top(賦值給scrollView.inset.top)
                CGPoint offset = self.scrollView.contentOffset;
                offset.y = -top;
                [self.scrollView setContentOffset:offset animated:NO];
            } completion:^(BOOL finished) {
                //執(zhí)行正在刷新的回調(diào)
                [self executeRefreshingCallback];
            }];
         });
    }
}

注意[super setState:state]的位置,等基類的state賦值給oldState,再跟新?tīng)顟B(tài)對(duì)比,對(duì)比完后,再[super setState:state]調(diào)用基類,從而賦值基類state

該方法主要要注意狀態(tài)在普通閑置狀態(tài)以及刷新?tīng)顟B(tài)的scrollView.contentOffset變化

三、MJRefreshStateHeader

該類是MJRefreshHeader的子類,主要用來(lái)設(shè)置顯示上一次刷新時(shí)間的label:lastUpdatedTimeLabel和顯示刷新?tīng)顟B(tài)的label:stateLabel屬性等

image
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初始化方法,實(shí)現(xiàn)本地化(不同字體),并根據(jù)不同狀態(tài)賦值給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
    //用戶定義的時(shí)間格式
    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] 會(huì)返回簡(jiǎn)體(英文、繁體)的 【最后更新:】
        //isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"" 如果上一次刷新也是今天,則返回簡(jiǎn)體(英文、繁體)的 【今天】,不是則返回空字符串
        self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@%@",
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
                                          isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"",
                                          time];
    } else {
    //沒(méi)有獲得上次更新時(shí)間
        self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@",
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];
    }
}

注意一下時(shí)間格式,本地化以及不同上次刷新時(shí)間的lastUpdatedTimeLabel顯示
上面代碼還給用戶自定義時(shí)間格式,沒(méi)有才使用默認(rèn),默認(rèn)的格式邏輯顯示,我已在上面注釋清楚

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

image

由此看來(lái),這兩種形式的header都有相同的共性,我們?cè)谧鲱愃频墓δ軙r(shí),如果有幾個(gè)控件或者幾個(gè)類共性一樣,比如說(shuō),一個(gè)保險(xiǎn)類(InsuranceClass),一個(gè)房地產(chǎn)類(RealEstateClass),他們可以有一個(gè)基類銷售類(SalesClass),SalesClass擁有銷售員工、顧客、金額、銷售日期等 保險(xiǎn)類 和 房地產(chǎn)類 需要的共同屬性

四、MJRefreshNormalHeader

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

2、布局這兩種樣式View,且在狀態(tài)切換時(shí)更改樣式切換

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

上面代碼主要實(shí)現(xiàn)了圈圈(菊花)和箭頭的布局,需要注意的是讓箭頭菊花緊跟刷新文字或者狀態(tài)文字居中的邏輯,我已在注釋寫(xiě)明

2、不同狀態(tài)下菊花和箭頭的互換
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStateIdle) {
        if (oldState == MJRefreshStateRefreshing) { //上次狀態(tài)是正在刷新,準(zhǔn)備改變成普通閑置狀態(tài)
            self.arrowView.transform = CGAffineTransformIdentity; //仿射變換初始化
            
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                self.loadingView.alpha = 0.0;  //把菊花變成完全透明
            } completion:^(BOOL finished) {
                // 如果執(zhí)行完動(dòng)畫(huà)發(fā)現(xiàn)不是idle狀態(tài),就直接返回,進(jìn)入其他狀態(tài)
                if (self.state != MJRefreshStateIdle) return;
//                self.loadingView.backgroundColor = [UIColor greenColor];
                self.loadingView.alpha = 1.0; //菊花變成完全顯示 (為什么要這樣?求大佬告訴)
                [self.loadingView stopAnimating]; //菊花停止轉(zhuǎn)動(dòng),同時(shí)會(huì)隱藏菊花(loadingView.hidesWhenStopped = YES;)
                self.arrowView.hidden = NO; //箭頭顯示
            }];
        } else { //上次狀態(tài)是拖拽或者普通閑置狀態(tài),準(zhǔn)備改變成普通閑置狀態(tài) --> 把菊花停止轉(zhuǎn)動(dòng),菊花隱藏,箭頭顯示
            [self.loadingView stopAnimating];
            self.arrowView.hidden = NO;
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                self.arrowView.transform = CGAffineTransformIdentity; //在操作結(jié)束之后對(duì)箭頭設(shè)置量進(jìn)行還原
            }];
        }
    } else if (state == MJRefreshStatePulling) { //拖拽狀態(tài):菊花停止轉(zhuǎn)動(dòng),箭頭顯示
        [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) { //正在刷新?tīng)顟B(tài):菊花完全顯示并且開(kāi)始轉(zhuǎn)動(dòng),箭頭隱藏
        self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動(dòng)畫(huà)完畢動(dòng)作沒(méi)有被執(zhí)行
        [self.loadingView startAnimating];
        self.arrowView.hidden = YES;
    }
}

通過(guò)不同的狀態(tài)控制菊花和箭頭的隱藏和消失,及他們的動(dòng)畫(huà)效果,如箭頭的朝上朝下,和菊花的轉(zhuǎn)與不轉(zhuǎn)

四、MJRefreshGifHeader

1、加載不同狀態(tài)對(duì)應(yīng)的動(dòng)畫(huà)圖片
2、設(shè)置不同狀態(tài)對(duì)應(yīng)的動(dòng)畫(huà)時(shí)間

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、設(shè)置不通過(guò)狀態(tài)對(duì)應(yīng)的動(dòng)畫(huà)圖片以及動(dòng)畫(huà)時(shí)間
#pragma mark - 公共方法
- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state 
{ 
    if (images == nil) return; 
    
    self.stateImages[@(state)] = images; 
    self.stateDurations[@(state)] = @(duration); 
    
    /* 根據(jù)圖片設(shè)置控件的高度 */ 
    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、實(shí)現(xiàn)圖片的切換和gifView布局
#pragma mark - 實(shí)現(xiàn)父類的方法
- (void)prepare
{
    [super prepare];
    
    // 初始化間距
    self.labelLeftInset = 20;
}

//根據(jù)拖拽進(jìn)度設(shè)置透明度
- (void)setPullingPercent:(CGFloat)pullingPercent
{
    [super setPullingPercent:pullingPercent];
    NSArray *images = self.stateImages[@(MJRefreshStateIdle)]; //選擇閑置狀態(tài)下的圖片組
    if (self.state != MJRefreshStateIdle || images.count == 0) return; //狀態(tài)不是閑置或者圖片為空,則直接返回
    // 停止動(dòng)畫(huà)
    [self.gifView stopAnimating];
    // 設(shè)置當(dāng)前需要顯示的圖片
    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沒(méi)有約束,直接返回
    
    self.gifView.frame = self.bounds;
    if (self.stateLabel.hidden && self.lastUpdatedTimeLabel.hidden) { //上次刷新時(shí)間和狀態(tài)文字都隱藏了,圖片內(nèi)容就居ifView中間顯示
        self.gifView.contentMode = UIViewContentModeCenter;
    } else { //圖片居gifView右邊顯示
        self.gifView.contentMode = UIViewContentModeRight;
        
        //下面代碼同樣也是為了讓gifView緊挨著文字居中顯示。算出最長(zhǎng)的文字,通過(guò)減去文字的一般寬度,調(diào)整gifView的x值,跟NormalHeader的方法一樣,詳細(xì)請(qǐng)看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
    
    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) { //狀態(tài)變?yōu)橥献Щ蛘哒谒⑿?,獲取各自狀態(tài)該顯示的圖片組
        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];
    }
}

到此,我對(duì)MJRefreshHeader那一塊的源碼已經(jīng)讀完,剩下MJRefreshFooter,但由于實(shí)現(xiàn)邏輯基本一致,故在此不再詳說(shuō)。遲點(diǎn),發(fā)現(xiàn)MJRefreshFooter有其他特殊之處,我會(huì)更新此文,謝謝大家!

學(xué)習(xí)

1、巧用Model

我們可能見(jiàn)到一些開(kāi)發(fā)者會(huì)在didSelectRowAtIndexPath協(xié)議方法里面這樣寫(xiě)

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

這樣會(huì)造成didSelectRowAtIndexPath方法過(guò)于臃腫,且重復(fù)代碼過(guò)多,太多if else 或者 switch,我們可以用Model很好的解決這個(gè)問(wèn)題,代碼如下:

- (NSArray *)examples
{
    if (!_examples) {
        MJExample *exam0 = [[MJExample alloc] init];
        exam0.header = MJExample00;
        exam0.vcClass = [MJTableViewController class];
        exam0.titles = @[@"默認(rèn)", @"動(dòng)畫(huà)圖片", @"隱藏時(shí)間", @"隱藏狀態(tài)和時(shí)間", @"自定義文字", @"自定義刷新控件"];
        exam0.methods = @[@"example01", @"example02", @"example03", @"example04", @"example05", @"example06"];
        
        MJExample *exam1 = [[MJExample alloc] init];
        exam1.header = MJExample10;
        exam1.vcClass = [MJTableViewController class];
        exam1.titles = @[@"默認(rèn)", @"動(dòng)畫(huà)圖片", @"隱藏刷新?tīng)顟B(tài)的文字", @"全部加載完畢", @"禁止自動(dòng)加載", @"自定義文字", @"加載后隱藏", @"自動(dòng)回彈的上拉01", @"自動(dòng)回彈的上拉02", @"自定義刷新控件(自動(dòng)刷新)", @"自定義刷新控件(自動(dòng)回彈)"];
        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、跳轉(zhuǎn)巧用

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

上面是跳轉(zhuǎn)方法,請(qǐng)留意[vc setValue:@"example01" forKeyPath:@"method"];這句代碼,下面會(huì)詳解

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(@"進(jìn)入此方法");
}

結(jié)果:


image

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

2、MJPerformSelectorLeakWarning( );如果selector是在運(yùn)行時(shí)才確定的,performSelector時(shí),若先把selector保存起來(lái),等到某事件發(fā)生后再調(diào)用,相當(dāng)于在動(dòng)態(tài)綁定之上再使用動(dòng)態(tài)綁定,不過(guò)這是編譯器不知道要執(zhí)行的selector是什么,因?yàn)檫@必須到了運(yùn)行時(shí)才能確定,使用這種特性的代價(jià)是,如果在ARC下編譯代碼,編譯器會(huì)發(fā)生警告,可用#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中為分類添加屬性的經(jīng)典用法,把上面跳轉(zhuǎn)方法中的[vc setValue:@"example01" forKeyPath:@"method"];賦值的example01 利用runtime關(guān)聯(lián),這樣分類中的method屬性值就為example01

解析一下 static char

比如有這樣一個(gè)函數(shù)
exp()
{
char a[] = "Hello!" ;
static char b[] = "Hello!" ;
}

當(dāng)調(diào)用這個(gè)函數(shù)完后,a[]就不存在了,而b[]依然存在,并且值為hello;

參考:

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

#pragma clang diagnostic ignored 用法

UIView常用的setNeedsDisplay和setNeedsLayout

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

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