MJRefresh源碼解析


簡(jiǎn)書博客已經(jīng)暫停更新,想看更多技術(shù)博客請(qǐng)到:

  • 掘金 :J_Knight_
  • 個(gè)人博客: J_Knight_
  • 個(gè)人公眾號(hào):程序員維他命

MJRefresh是李明杰老師的作品衷敌,到現(xiàn)在已經(jīng)有9800多顆star了荆陆,是一個(gè)簡(jiǎn)單實(shí)用吼和,功能強(qiáng)大的iOS下拉刷新(也支持上拉加載更多)控件洛口。它的可定制性很高影兽,幾乎可以滿足大部分下拉刷新的設(shè)計(jì)需求,值得學(xué)習(xí)舰始。

該框架的結(jié)構(gòu)設(shè)計(jì)得很清晰崇棠,使用一個(gè)基類MJRefreshComponent來做一些基本的設(shè)定,然后通過繼承的方式丸卷,讓MJRefreshHeaderMJRefreshFooter分別具備下拉刷新和上拉加載的功能枕稀。從繼承機(jī)構(gòu)來看可以分為三層,具體可以從下面的圖里看出來:

框架組織結(jié)構(gòu)圖

首先來看一下該控件的基類:MJRefreshComponent:

MJRefreshComponent

這個(gè)類作為該控件的基類谜嫉,涵蓋了基類所具備的一些:狀態(tài)萎坷,回調(diào)block等,大致分成下面這5種職能:

有哪些職能?

  1. 聲明控件的所有狀態(tài)沐兰。
  2. 聲明控件的回調(diào)函數(shù)哆档。
  3. 添加監(jiān)聽。
  4. 提供刷新住闯,停止刷新接口瓜浸。
  5. 提供子類需要實(shí)現(xiàn)的方法。

職能如何實(shí)現(xiàn)比原?

1. 聲明控件的所有狀態(tài)

/** 刷新控件的狀態(tài) */
typedef NS_ENUM(NSInteger, MJRefreshState) {
    /** 普通閑置狀態(tài) */
    MJRefreshStateIdle = 1,
    /** 松開就可以進(jìn)行刷新的狀態(tài) */
    MJRefreshStatePulling,
    /** 正在刷新中的狀態(tài) */
    MJRefreshStateRefreshing,
    /** 即將刷新的狀態(tài) */
    MJRefreshStateWillRefresh,
    /** 所有數(shù)據(jù)加載完畢插佛,沒有更多的數(shù)據(jù)了 */
    MJRefreshStateNoMoreData
};

2. 聲明控件的回調(diào)函數(shù)

/** 進(jìn)入刷新狀態(tài)的回調(diào) */
typedef void (^MJRefreshComponentRefreshingBlock)();
/** 開始刷新后的回調(diào)(進(jìn)入刷新狀態(tài)后的回調(diào)) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)();
/** 結(jié)束刷新后的回調(diào) */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)();

3. 添加監(jiān)聽

監(jiān)聽的聲明:

- (void)addObservers
{
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];//contentOffset屬性
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];//contentSize屬性
    self.pan = self.scrollView.panGestureRecognizer;
    [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];//UIPanGestureRecognizer 的state屬性
}

對(duì)于監(jiān)聽的處理:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // 遇到這些情況就直接返回
    if (!self.userInteractionEnabled) return;
    
    // 這個(gè)就算看不見也需要處理
    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];
    }
}

4. 提供刷新,停止刷新接口

#pragma mark 進(jìn)入刷新狀態(tài)

- (void)beginRefreshingWithCompletionBlock:(void (^)())completionBlock
{
    self.beginRefreshingCompletionBlock = completionBlock;
    
    [self beginRefreshing];
}

- (void)beginRefreshing
{
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
        self.alpha = 1.0;
    }];
    self.pullingPercent = 1.0;
    // 只要正在刷新量窘,就完全顯示
    if (self.window) {
        //將狀態(tài)切換為正在刷新
        self.state = MJRefreshStateRefreshing;
    } else {
        // 預(yù)防正在刷新中時(shí)雇寇,調(diào)用本方法使得header inset回置失敗
        if (self.state != MJRefreshStateRefreshing) {
            //將狀態(tài)切換為即將刷新
            self.state = MJRefreshStateWillRefresh;
            // 刷新(預(yù)防從另一個(gè)控制器回到這個(gè)控制器的情況,回來要重新刷新一下)
            [self setNeedsDisplay];
        }
    }
}


#pragma mark 結(jié)束刷新狀態(tài)
- (void)endRefreshing
{
    self.state = MJRefreshStateIdle;
}

- (void)endRefreshingWithCompletionBlock:(void (^)())completionBlock
{
    self.endRefreshingCompletionBlock = completionBlock;
    
    [self endRefreshing];
}

#pragma mark 是否正在刷新
- (BOOL)isRefreshing
{
    return self.state == MJRefreshStateRefreshing || self.state == MJRefreshStateWillRefresh;
}

交給子類實(shí)現(xiàn)的方法:

- (void)prepare NS_REQUIRES_SUPER;
/** 擺放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的contentOffset發(fā)生改變的時(shí)候調(diào)用 */
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的contentSize發(fā)生改變的時(shí)候調(diào)用 */
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的拖拽狀態(tài)發(fā)生改變的時(shí)候調(diào)用 */
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;

5. 提供子類需要實(shí)現(xiàn)的方法

#pragma mark - 交給子類們?nèi)?shí)現(xiàn)
/** 初始化 */
- (void)prepare NS_REQUIRES_SUPER;
/** 擺放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的contentOffset發(fā)生改變的時(shí)候調(diào)用 */
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的contentSize發(fā)生改變的時(shí)候調(diào)用 */
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的拖拽狀態(tài)發(fā)生改變的時(shí)候調(diào)用 */
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;

從上面等結(jié)構(gòu)圖可以看出蚌铜,緊接著這個(gè)基類谢床,下面分為MJRefreshHeaderMJRefreshFooter,這里順著MJRefreshHeader這個(gè)分支向下展開:

MJRefreshHeader

MJRefreshHeader繼承于MJRefreshComponent,它做了這幾件事:

有哪些職能厘线?

  1. 初始化识腿。
  2. 設(shè)置header高度。
  3. 重新調(diào)整y值造壮。
  4. 根據(jù)contentOffset的變化渡讼,來切換狀態(tài)(默認(rèn)狀態(tài),可以刷新的狀態(tài)耳璧,正在刷新的狀態(tài))成箫,實(shí)現(xiàn)方法是:scrollViewContentOffsetDidChange:
  5. 在切換狀態(tài)時(shí)旨枯,執(zhí)行相應(yīng)的操作蹬昌。實(shí)現(xiàn)方法是:setState:

職能如何實(shí)現(xiàn)攀隔?

1. 初始化

初始化有兩種方法:

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
    MJRefreshHeader *cmp = [[self alloc] init];
    //傳入block
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    MJRefreshHeader *cmp = [[self alloc] init];
    //設(shè)置self.refreshingTarget 和 self.refreshingAction
    [cmp setRefreshingTarget:target refreshingAction:action];
    return cmp;
}

2. 設(shè)置header高度

通過重寫prepare方法來設(shè)置header的高度:

- (void)prepare
{
    [super prepare];
    
    // 設(shè)置用于在NSUserDefaults里存儲(chǔ)時(shí)間的key
    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
    
    // 設(shè)置header的高度
    self.mj_h = MJRefreshHeaderHeight;
}

3. 重新調(diào)整y值

通過重寫placeSubviews方法來重新調(diào)整y值:

- (void)placeSubviews
{
    [super placeSubviews];
    
    // 設(shè)置y值(當(dāng)自己的高度發(fā)生改變了皂贩,肯定要重新調(diào)整Y值栖榨,所以放到placeSubviews方法中設(shè)置y值)
    self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
    //self.ignoredScrollViewContentInsetTop 如果是10,那么就向上移動(dòng)10
}

4. 狀態(tài)切換的代碼:

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    // 正在刷新的狀態(tài)
    if (self.state == MJRefreshStateRefreshing) {
        
        if (self.window == nil) return;
        
        //- self.scrollView.mj_offsetY:-(-54-64)= 118 : 刷新的時(shí)候明刷,偏移量是不動(dòng)的婴栽。偏移量 = 狀態(tài)欄 + 導(dǎo)航欄 + header的高度
        //_scrollViewOriginalInset.top:64 (狀態(tài)欄 + 導(dǎo)航欄)
        //insetT 取二者之間大的那一個(gè)
        CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
       
        //118
        insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
        
        //設(shè)置contentInset
        self.scrollView.mj_insetT = insetT;
        
        // 記錄刷新的時(shí)候的偏移量 -54 = 64 - 118
        self.insetTDelta = _scrollViewOriginalInset.top - insetT;
        
        return;
    }
    
    // 跳轉(zhuǎn)到下一個(gè)控制器時(shí),contentInset可能會(huì)變
     _scrollViewOriginalInset = self.scrollView.contentInset;
    
    // 記錄當(dāng)前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;

    // 頭部控件剛好全部出現(xiàn)的offsetY,默認(rèn)是-64(20 + 44)
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 向上滾動(dòng)辈末,直接返回
    if (offsetY > happenOffsetY) return;
    
    // 從普通 到 即將刷新 的臨界距離
    CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;// -64 - 54 = -118
    
    //下拉的百分比:下拉的距離與header高度的比值
    CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
    
    if (self.scrollView.isDragging) {
        
        //記錄當(dāng)前下拉的百分比
        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) {
            // 如果當(dāng)前狀態(tài)為可以刷新 && 下拉的距離小于臨界距離,則將狀態(tài)切換為默認(rèn)
            self.state = MJRefreshStateIdle;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
        // 手松開 && 狀態(tài)為可以刷新(MJRefreshStatePulling)時(shí) 開始刷新
        [self beginRefreshing];
        
    } else if (pullingPercent < 1) {
        //手松開后挤聘,默認(rèn)狀態(tài)時(shí)轰枝,恢復(fù)self.pullingPercent
        self.pullingPercent = pullingPercent;        
    }
}

需要注意三點(diǎn):

  1. 這里的狀態(tài)有三種:默認(rèn)狀態(tài)(MJRefreshStateIdle),可以刷新的狀態(tài)(MJRefreshStatePulling)以及正在刷新的狀態(tài)(MJRefreshStateRefreshing)组去。
  2. 狀態(tài)切換的因素有兩個(gè):一個(gè)是下拉的距離是否超過臨界值鞍陨,另一個(gè)是 手指是否離開屏幕。
  3. 注意:可以刷新的狀態(tài)正在刷新的狀態(tài)是不同的添怔。因?yàn)樵谑种高€貼在屏幕的時(shí)候是不能進(jìn)行刷新的湾戳。所以即使在下拉的距離超過了臨界距離(狀態(tài)欄 + 導(dǎo)航欄 + header高度)贤旷,如果手指沒有離開屏幕广料,那么也不能馬上進(jìn)行刷新,而是將狀態(tài)切換為:可以刷新幼驶。一旦手指離開了屏幕艾杏,馬上將狀態(tài)切換為正在刷新。

這里提供一張圖來體現(xiàn)三個(gè)狀態(tài)的不同:


三個(gè)狀態(tài)

5. 狀態(tài)切換時(shí)的相應(yīng)操作:

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
   
    if (state == MJRefreshStateIdle) {
       
        //============== 設(shè)置狀態(tài)為默認(rèn)狀態(tài) =============//
        
        //如果當(dāng)前不是正在刷新就返回盅藻,因?yàn)檫@個(gè)方法主要針對(duì)從正在刷新狀態(tài)(oldstate)到默認(rèn)狀態(tài)
        if (oldState != MJRefreshStateRefreshing) return;
        
        //刷新完成后购桑,保存刷新完成的時(shí)間
        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
        // 恢復(fù)inset和offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            
            //118 -> 64(剪去了header的高度)
            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) {
                //調(diào)用刷新完成的block
                self.endRefreshingCompletionBlock();
            }
        }];
        
    } else if (state == MJRefreshStateRefreshing) {
        
         //============== 設(shè)置狀態(tài)為正在刷新狀態(tài) =============//
         dispatch_async(dispatch_get_main_queue(), ^{
            
             [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
               
                CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;//64 + 54 (都是默認(rèn)的高度)
                // 重新設(shè)置contentInset,top = 118
                self.scrollView.mj_insetT = top;
                // 設(shè)置滾動(dòng)位置
                [self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
                 
            } completion:^(BOOL finished) {
                //調(diào)用進(jìn)行刷新的block
                [self executeRefreshingCallback];
            }];
         });
    }
}

這里需要注意兩點(diǎn):

  1. 這里狀態(tài)的切換氏淑,主要圍繞著兩種:默認(rèn)狀態(tài)和正在刷新狀態(tài)勃蜘。也就是針對(duì)開始刷新結(jié)束刷新這兩個(gè)切換點(diǎn)。
  2. 從正在刷新狀態(tài)狀態(tài)切換為默認(rèn)狀態(tài)時(shí)(結(jié)束刷新)假残,需要記錄刷新結(jié)束的時(shí)間缭贡。因?yàn)閔eader里面有一個(gè)默認(rèn)的label是用來顯示上次刷新的時(shí)間的。

MJRefreshStateHeader

這個(gè)類是MJRefreshHeader類的子類辉懒,它做了兩件事:

有哪些職能阳惹?

  1. 簡(jiǎn)單布局了stateLabellastUpdatedTimeLabel
  2. 根據(jù)控件狀態(tài)的切換(默認(rèn)狀態(tài)眶俩,正在刷新狀態(tài))莹汤,實(shí)現(xiàn)了這兩個(gè)label顯示的文字的切換。

給一張圖颠印,讓大家直觀感受一下這兩個(gè)控件:

兩個(gè)Label

職能如何實(shí)現(xiàn)纲岭?

這個(gè)類通過覆蓋父類三個(gè)方法來實(shí)現(xiàn)上述兩個(gè)實(shí)現(xiàn):

方法1: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];
}

在這里抹竹,將每一個(gè)狀態(tài)對(duì)應(yīng)的提示文字放入一個(gè)字典里面,key是狀態(tài)的NSNumber形式

- (void)setTitle:(NSString *)title forState:(MJRefreshState)state
{
    if (title == nil) return;
    self.stateTitles[@(state)] = title;
    self.stateLabel.text = self.stateTitles[@(self.state)];
}

方法2:placeSubviews方法

- (void)placeSubviews
{
    [super placeSubviews];
    
    if (self.stateLabel.hidden) return;
    
    BOOL noConstrainsOnStatusLabel = self.stateLabel.constraints.count == 0;
    
    if (self.lastUpdatedTimeLabel.hidden) {
        
        //如果更新時(shí)間label是隱藏的,則讓狀態(tài)label撐滿整個(gè)header
        if (noConstrainsOnStatusLabel) self.stateLabel.frame = self.bounds;
        
    } else {
        
        //如果更新時(shí)間label不是隱藏的荒勇,根據(jù)約束設(shè)置更新時(shí)間label和狀態(tài)label(高度各占一半)
        CGFloat stateLabelH = self.mj_h * 0.5;
        
        if (noConstrainsOnStatusLabel) {
            self.stateLabel.mj_x = 0;
            self.stateLabel.mj_y = 0;
            self.stateLabel.mj_w = self.mj_w;
            self.stateLabel.mj_h = stateLabelH;
        }
        
        // 更新時(shí)間label
        if (self.lastUpdatedTimeLabel.constraints.count == 0) {
            self.lastUpdatedTimeLabel.mj_x = 0;
            self.lastUpdatedTimeLabel.mj_y = stateLabelH;
            self.lastUpdatedTimeLabel.mj_w = self.mj_w;
            self.lastUpdatedTimeLabel.mj_h = self.mj_h - self.lastUpdatedTimeLabel.mj_y;
        }
    }
}

這里主要是對(duì)lastUpdatedTimeLabelstateLabel進(jìn)行布局柒莉。要注意lastUpdatedTimeLabel隱藏的情況。

方法3: setState:方法

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

在這里沽翔,根據(jù)傳入的state的不同兢孝,在stateLabellastUpdatedTimeLabel里切換相應(yīng)的文字。

  • stateLabel里的文字直接從stateTitles字典里取出即可仅偎。
  • lastUpdatedTimeLabel里的文字需要通過一個(gè)方法來取出即可:
- (void)setLastUpdatedTimeKey:(NSString *)lastUpdatedTimeKey
{
    [super setLastUpdatedTimeKey:lastUpdatedTimeKey];
    
    // 如果label隱藏了跨蟹,就不用再處理
    if (self.lastUpdatedTimeLabel.hidden) return;
    
    //根據(jù)key,從NSUserDefaults獲取對(duì)應(yīng)的NSData型時(shí)間
    NSDate *lastUpdatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:lastUpdatedTimeKey];
    
    // 如果有block橘沥,從block里拿來時(shí)間窗轩,這應(yīng)該是用戶自定義顯示時(shí)間格式的渠道
    if (self.lastUpdatedTimeText) {
        self.lastUpdatedTimeLabel.text = self.lastUpdatedTimeText(lastUpdatedTime);
        return;
    }
    
    //如果沒有block,就按照下面的默認(rèn)方法顯示時(shí)間格式
    if (lastUpdatedTime) {
        
        // 獲得了上次更新時(shí)間
        // 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";
            isToday = YES;
            
        } else if ([cmp1 year] == [cmp2 year]) { // 今年
            //今年痢艺,省去年,顯示月日
            formatter.dateFormat = @"MM-dd HH:mm";
        } else {
            //其他介陶,年月日都顯示
            formatter.dateFormat = @"yyyy-MM-dd HH:mm";
        }
        NSString *time = [formatter stringFromDate:lastUpdatedTime];
        
        // 3.顯示日期
        self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@%@",
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
                                          isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"",
                                          time];
    } else {
        // 沒有獲得上次更新時(shí)間(應(yīng)該是第一次更新或者多次更新堤舒,之前的更新都失敗了)
        self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@",
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];
    }
}

在這里注意兩點(diǎn):

  1. 作者通過使用block來讓用戶自己定義日期現(xiàn)實(shí)的格式,如果用戶沒有自定義哺呜,就使用作者提供的默認(rèn)格式舌缤。
  2. 在默認(rèn)格式的設(shè)置里,判斷了是否是今日某残,是否是今年的情況国撵。在以后設(shè)計(jì)顯示時(shí)間的labe的時(shí)候可以借鑒一下。

MJRefreshNormalHeader

有哪些職能玻墅?

MJRefreshNormalHeader 繼承于 MJRefreshStateHeader介牙,它主要做了兩件事:

  1. 它在MJRefreshStateHeader上添加了_arrowViewloadingView
  2. 布局了這兩個(gè)view并在Refresh控件的狀態(tài)切換的時(shí)候改變這兩個(gè)view的樣式澳厢。

還是給一張圖來直觀感受一下這兩個(gè)view:

兩個(gè)view

職能如何實(shí)現(xiàn)环础?

同MJRefreshStateHeader一樣,也是重寫了父類的三個(gè)方法:

方法1:prepare

- (void)prepare
{
    [super prepare];
    
    self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
}

方法2:placeSubviews

- (void)placeSubviews
{
    [super placeSubviews];
    
    // 首先將箭頭的中心點(diǎn)x設(shè)為header寬度的一半
    CGFloat arrowCenterX = self.mj_w * 0.5;
    
    if (!self.stateLabel.hidden) {
        
        CGFloat stateWidth = self.stateLabel.mj_textWith;
        CGFloat timeWidth = 0.0;
        if (!self.lastUpdatedTimeLabel.hidden) {
            timeWidth = self.lastUpdatedTimeLabel.mj_textWith;
        }
        
        //在stateLabel里的文字寬度和更新時(shí)間里的文字寬度里取較寬的
        CGFloat textWidth = MAX(stateWidth, timeWidth);
        //根據(jù)self.labelLeftInset和textWidth向左移動(dòng)中心點(diǎn)x
        arrowCenterX -= textWidth / 2 + self.labelLeftInset;
    }
    
    //中心點(diǎn)y永遠(yuǎn)設(shè)置為header的高度的一半
    CGFloat arrowCenterY = self.mj_h * 0.5;
    
    //獲得了最終的center赏酥,而這個(gè)center同時(shí)適用于arrowView和loadingView喳整,因?yàn)槎呤遣还泊娴摹?    CGPoint arrowCenter = CGPointMake(arrowCenterX, arrowCenterY);
    
    // 箭頭
    if (self.arrowView.constraints.count == 0) {
        //控件大小等于圖片大小
        self.arrowView.mj_size = self.arrowView.image.size;
        self.arrowView.center = arrowCenter;
    }
        
    // 菊花
    if (self.loadingView.constraints.count == 0) {
        self.loadingView.center = arrowCenter;
    }
    
    //arrowView的色調(diào)與stateLabel的字體顏色一致
    self.arrowView.tintColor = self.stateLabel.textColor;
}

在這里注意一點(diǎn):因?yàn)?code>stateLabel和lastUpdatedTimeLabel是上下并排分布的,而arrowViewloadingView是在這二者的左邊裸扶,所以為了避免這兩組重合框都,在計(jì)算arrowViewloadingView的center的時(shí)候,需要獲取stateLabellastUpdatedTimeLabel兩個(gè)控件的寬度并比較大小,將較大的一個(gè)作為兩個(gè)label的‘最寬距離’魏保,再計(jì)算center熬尺,這樣一來就不會(huì)重合了。
而對(duì)于如何計(jì)算寬度谓罗,作者給出了一個(gè)方案粱哼,大家可以在以后的實(shí)踐中使用:

- (CGFloat)mj_textWith {
    CGFloat stringWidth = 0;
    CGSize size = CGSizeMake(MAXFLOAT, MAXFLOAT);
    if (self.text.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
        stringWidth =[self.text
                      boundingRectWithSize:size
                      options:NSStringDrawingUsesLineFragmentOrigin
                      attributes:@{NSFontAttributeName:self.font}
                      context:nil].size.width;
#else
        
        stringWidth = [self.text sizeWithFont:self.font
                             constrainedToSize:size
                                 lineBreakMode:NSLineBreakByCharWrapping].width;
#endif
    }
    return stringWidth;
}

方法3: setState:

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根據(jù)狀態(tài)更新arrowView和loadingView的顯示
    if (state == MJRefreshStateIdle) {
       
        //1. 設(shè)置為默認(rèn)狀態(tài)
        if (oldState == MJRefreshStateRefreshing) {
            
            //1.1 從正在刷新狀態(tài)中切換過來
            self.arrowView.transform = CGAffineTransformIdentity;
            
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                //隱藏菊花
                self.loadingView.alpha = 0.0;
                
            } completion:^(BOOL finished) {
                
                // 如果執(zhí)行完動(dòng)畫發(fā)現(xiàn)不是idle狀態(tài),就直接返回檩咱,進(jìn)入其他狀態(tài)
                if (self.state != MJRefreshStateIdle) return;
                //菊花停止旋轉(zhuǎn)
                self.loadingView.alpha = 1.0;
                [self.loadingView stopAnimating];
                //顯示箭頭
                self.arrowView.hidden = NO;
            }];
            
        } else {
            //1.2 從其他狀態(tài)中切換過來
            [self.loadingView stopAnimating];
            //顯示箭頭并設(shè)置為初始狀態(tài)
            self.arrowView.hidden = NO;
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                self.arrowView.transform = CGAffineTransformIdentity;
            }];
        }
        
    } else if (state == MJRefreshStatePulling) {
        
        //2. 設(shè)置為可以刷新狀態(tài)
        [self.loadingView stopAnimating];
        self.arrowView.hidden = NO;
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
            //箭頭倒立
            self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
        }];
        
    } else if (state == MJRefreshStateRefreshing) {
        
        //3. 設(shè)置為正在刷新狀態(tài)
        self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動(dòng)畫完畢動(dòng)作沒有被執(zhí)行
        //菊花旋轉(zhuǎn)
        [self.loadingView startAnimating];
        //隱藏arrowView
        self.arrowView.hidden = YES;
    }
}

到此為止揭措,我們已經(jīng)從MJRefreshComponentMJRefreshNormalHeader的實(shí)現(xiàn)過程看了一遍】舔牵可以看出绊含,作者將prepare,placeSubviews以及setState:方法作為基類的方法,讓下面的子類去一層一層實(shí)現(xiàn)炊汹。

而每一層的子類躬充,根據(jù)自身的職責(zé),分別按照自己的方式來實(shí)現(xiàn)這三個(gè)方法:

  • MJRefreshHeader: 負(fù)責(zé)header的高度和調(diào)整header自身在外部的位置讨便。
  • MJRefreshStateHeader:負(fù)責(zé)header內(nèi)部的stateLabellastUpdatedTimeLabel的布局和不同狀態(tài)下內(nèi)部文字的顯示充甚。
  • MJRefreshNormalHeader:負(fù)責(zé)header內(nèi)部的loadingView以及arrowView的布局和不同狀態(tài)下的顯示。

這樣做的好處是霸褒,如果想要增加某種類型的header伴找,只要在某一層上做文章即可。例如該框架里的MJRefreshGifHeader,它和MJRefreshNormalHeader屬于同一級(jí)傲霸,都是繼承于MJRefreshStateHeader疆瑰。因?yàn)槎叨季哂邢嗤问降?code>stateLabel和lastUpdatedTimeLabel眉反,唯一不同的就是左側(cè)的部分:

  • MJRefreshNormalHeader的左側(cè)是箭頭昙啄。
  • MJRefreshGifHeader的左側(cè)則是一個(gè)gif動(dòng)畫。

還是提供一張圖來直觀感受一下:


normalHeader 與 gifHeader

下面我們來看一下的實(shí)現(xiàn):

MJRefreshGifHeader

它提供了兩個(gè)接口寸五,是用來設(shè)置不同狀態(tài)下使用的圖片數(shù)組的:

- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state 
{ 
    if (images == nil) return; 
    
    //設(shè)置不同狀態(tài)下的圖片組和持續(xù)時(shí)間
    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 
{ 
   //如果沒有傳入duration梳凛,則根據(jù)圖片的多少來計(jì)算
    [self setImages:images duration:images.count * 0.1 forState:state]; 
}

有哪些職能?

然后梳杏,和MJRefreshNormalHeader一樣韧拒,它也重寫了基類提供的三個(gè)方法來實(shí)現(xiàn)顯示gif圖片的職能。

職能如何實(shí)現(xiàn)十性?

1. 初始化和label的間距

- (void)prepare
{
    [super prepare];
    
    // 初始化間距
    self.labelLeftInset = 20;
}

2. 根據(jù)label的寬度和存在與否設(shè)置gif的位置

- (void)placeSubviews
{
    [super placeSubviews];
    
    //如果約束存在叛溢,就立即返回
    if (self.gifView.constraints.count) return;
    
    self.gifView.frame = self.bounds;
    
    if (self.stateLabel.hidden && self.lastUpdatedTimeLabel.hidden) {
        
        //如果stateLabel和lastUpdatedTimeLabel都在隱藏狀態(tài),將gif劇中顯示
        self.gifView.contentMode = UIViewContentModeCenter;
        
    } else {
        
        //如果stateLabel和lastUpdatedTimeLabel中至少一個(gè)存在劲适,則根據(jù)label的寬度設(shè)置gif的位置
        self.gifView.contentMode = UIViewContentModeRight;
        
        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;
    }
}

3. 根據(jù)傳入狀態(tài)的不同來設(shè)置動(dòng)畫

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {
        
        //1. 如果傳進(jìn)來的狀態(tài)是可以刷新和正在刷新
        NSArray *images = self.stateImages[@(state)];
        if (images.count == 0) return;
        
        [self.gifView stopAnimating];
        
        if (images.count == 1) {
            //1.1 單張圖片
            self.gifView.image = [images lastObject];
        } else {
            //1.2 多張圖片
            self.gifView.animationImages = images;
            self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
            [self.gifView startAnimating];
        }
    } else if (state == MJRefreshStateIdle) {
        //2.如果傳進(jìn)來的狀態(tài)是默認(rèn)狀態(tài)
        [self.gifView stopAnimating];
    }
}

Footer類是用來處理上拉加載的楷掉,實(shí)現(xiàn)原理和下拉刷新很類似,在這里先不介紹了~

總的來說霞势,該框架設(shè)計(jì)得非常工整:通過一個(gè)基類來定義一些狀態(tài)和一些需要子類實(shí)現(xiàn)的接口烹植。通過一層一層地繼承斑鸦,讓每一層的子類各司其職,只完成真正屬于自己的任務(wù)草雕,提高了框架的可定制性巷屿,而且對(duì)于功能的擴(kuò)展和bug的追蹤也很有幫助,非常值得我們參考與借鑒墩虹。

本篇文章已經(jīng)同步到我個(gè)人博客:J_Knight MJRefresh 源碼解析

歡迎來參觀 ^^

本文已在版權(quán)印備案嘱巾,如需轉(zhuǎn)載請(qǐng)?jiān)L問版權(quán)印。48422928

獲取授權(quán)

-------------------------------- 2018年7月17日更新 --------------------------------

注意注意=氲觥E啊!

筆者在近期開通了個(gè)人公眾號(hào)尖坤,主要分享編程稳懒,讀書筆記,思考類的文章慢味。

  • 編程類文章:包括筆者以前發(fā)布的精選技術(shù)文章场梆,以及后續(xù)發(fā)布的技術(shù)文章(以原創(chuàng)為主),并且逐漸脫離 iOS 的內(nèi)容纯路,將側(cè)重點(diǎn)會(huì)轉(zhuǎn)移到提高編程能力的方向上或油。
  • 讀書筆記類文章:分享編程類思考類驰唬,心理類顶岸,職場(chǎng)類書籍的讀書筆記。
  • 思考類文章:分享筆者平時(shí)在技術(shù)上叫编,生活上的思考辖佣。

因?yàn)楣娞?hào)每天發(fā)布的消息數(shù)有限制,所以到目前為止還沒有將所有過去的精選文章都發(fā)布在公眾號(hào)上搓逾,后續(xù)會(huì)逐步發(fā)布的卷谈。

而且因?yàn)楦鞔蟛┛推脚_(tái)的各種限制,后面還會(huì)在公眾號(hào)上發(fā)布一些短小精干霞篡,以小見大的干貨文章哦~

掃下方的公眾號(hào)二維碼并點(diǎn)擊關(guān)注世蔗,期待與您的共同成長(zhǎng)~

公眾號(hào):程序員維他命
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市朗兵,隨后出現(xiàn)的幾起案子污淋,更是在濱河造成了極大的恐慌,老刑警劉巖余掖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寸爆,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)而昨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門救氯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人歌憨,你說我怎么就攤上這事着憨。” “怎么了务嫡?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵甲抖,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我心铃,道長(zhǎng)准谚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任去扣,我火速辦了婚禮柱衔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘愉棱。我一直安慰自己唆铐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布奔滑。 她就那樣靜靜地躺著艾岂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪朋其。 梳的紋絲不亂的頭發(fā)上王浴,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音梅猿,去河邊找鬼氓辣。 笑死,一個(gè)胖子當(dāng)著我的面吹牛粒没,可吹牛的內(nèi)容都是我干的筛婉。 我是一名探鬼主播簇爆,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼癞松,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了入蛆?” 一聲冷哼從身側(cè)響起响蓉,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哨毁,沒想到半個(gè)月后枫甲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年想幻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了粱栖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡脏毯,死狀恐怖闹究,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情食店,我是刑警寧澤渣淤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站吉嫩,受9級(jí)特大地震影響价认,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜自娩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一用踩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧忙迁,春花似錦捶箱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搬男。三九已至伦意,卻和暖如春举塔,著一層夾襖步出監(jiān)牢的瞬間孟辑,已是汗流浹背车柠。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工立砸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留洞坑,地道東北人缠局。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓呀页,卻偏偏與公主長(zhǎng)得像妈拌,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蓬蝶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • MJRefresh 是著名開發(fā)者及培訓(xùn)講師李明杰老師的作品尘分,到現(xiàn)在在github已經(jīng)有10000多顆star,真真...
    藍(lán)色小石頭閱讀 4,392評(píng)論 6 33
  • 本文轉(zhuǎn)載自J_Knight 的MJRefresh源碼解析 MJRefresh是李明杰的作品丸氛,到現(xiàn)在已經(jīng)有9800多...
    Detective41閱讀 657評(píng)論 0 1
  • 今天來讀李明杰老師的MJRefresh框架培愁,老聽他講的課,代碼風(fēng)格也好缓窜,讓我們一起學(xué)習(xí)下這個(gè)框架吧定续。源碼的下載地址...
    charlotte2018閱讀 331評(píng)論 0 1
  • 兩位心理學(xué)家推測(cè)谍咆,現(xiàn)場(chǎng)有大量其他旁觀者在場(chǎng)時(shí),旁觀者對(duì)緊急情況伸出援手的可能性最低私股,原因至少有兩個(gè)摹察。第一個(gè)原因很淺...
    Fly_Catkin閱讀 1,341評(píng)論 0 1
  • 終于下定決心去剪發(fā)。 長(zhǎng)發(fā)已經(jīng)陪伴了我十五年倡鲸。從二十歲到三十五歲港粱。生命中最美好的歲月,是和長(zhǎng)發(fā)一起走過的旦签。其間查坪,拉...
    燕影閱讀 368評(píng)論 0 0