MJRefresh源碼解析


簡(jiǎn)書(shū)博客已經(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下拉刷新(也支持上拉加載更多)控件。它的可定制性很高,幾乎可以滿(mǎn)足大部分下拉刷新的設(shè)計(jì)需求,值得學(xué)習(xí)。

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

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

首先來(lái)看一下該控件的基類(lèi):MJRefreshComponent:

MJRefreshComponent

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

有哪些職能?

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

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

1. 聲明控件的所有狀態(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
};

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

/** 進(jìn)入刷新?tīng)顟B(tài)的回調(diào) */
typedef void (^MJRefreshComponentRefreshingBlock)();
/** 開(kāi)始刷新后的回調(diào)(進(jìn)入刷新?tīng)顟B(tài)后的回調(diào)) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)();
/** 結(jié)束刷新后的回調(diào) */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)();

3. 添加監(jiān)聽(tīng)

監(jiān)聽(tīng)的聲明:

- (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)聽(tīng)的處理:

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

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

#pragma mark 進(jìn)入刷新?tīng)顟B(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è)控制器的情況,回來(lái)要重新刷新一下)
            [self setNeedsDisplay];
        }
    }
}


#pragma mark 結(jié)束刷新?tīng)顟B(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;
}

交給子類(lè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;

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

#pragma mark - 交給子類(lèi)們?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è)基類(lèi),下面分為MJRefreshHeaderMJRefreshFooter,這里順著MJRefreshHeader這個(gè)分支向下展開(kāi):

MJRefreshHeader

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

有哪些職能?

  1. 初始化。
  2. 設(shè)置header高度。
  3. 重新調(diào)整y值。
  4. 根據(jù)contentOffset的變化,來(lái)切換狀態(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高度

通過(guò)重寫(xiě)prepare方法來(lái)設(shè)置header的高度:

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

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

通過(guò)重寫(xiě)placeSubviews方法來(lái)重新調(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) {// 即將刷新 && 手松開(kāi)
        // 手松開(kāi) && 狀態(tài)為可以刷新(MJRefreshStatePulling)時(shí) 開(kāi)始刷新
        [self beginRefreshing];
        
    } else if (pullingPercent < 1) {
        //手松開(kāi)后,默認(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è)是下拉的距離是否超過(guò)臨界值,另一個(gè)是 手指是否離開(kāi)屏幕。
  3. 注意:可以刷新的狀態(tài)正在刷新的狀態(tài)是不同的。因?yàn)樵谑种高€貼在屏幕的時(shí)候是不能進(jìn)行刷新的。所以即使在下拉的距離超過(guò)了臨界距離(狀態(tài)欄 + 導(dǎo)航欄 + header高度),如果手指沒(méi)有離開(kāi)屏幕,那么也不能馬上進(jìn)行刷新,而是將狀態(tài)切換為:可以刷新。一旦手指離開(kāi)了屏幕,馬上將狀態(tài)切換為正在刷新。

這里提供一張圖來(lá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īng)顟B(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īng)顟B(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īng)顟B(tài)。也就是針對(duì)開(kāi)始刷新結(jié)束刷新這兩個(gè)切換點(diǎn)。
  2. 從正在刷新?tīng)顟B(tài)狀態(tài)切換為默認(rèn)狀態(tài)時(shí)(結(jié)束刷新),需要記錄刷新結(jié)束的時(shí)間。因?yàn)閔eader里面有一個(gè)默認(rèn)的label是用來(lái)顯示上次刷新的時(shí)間的。

MJRefreshStateHeader

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

有哪些職能?

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

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

兩個(gè)Label

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

這個(gè)類(lèi)通過(guò)覆蓋父類(lèi)三個(gè)方法來(lái)實(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撐滿(mǎn)整個(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里的文字需要通過(guò)一個(gè)方法來(lái)取出即可:
- (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里拿來(lái)時(shí)間,這應(yīng)該是用戶(hù)自定義顯示時(shí)間格式的渠道
    if (self.lastUpdatedTimeText) {
        self.lastUpdatedTimeLabel.text = self.lastUpdatedTimeText(lastUpdatedTime);
        return;
    }
    
    //如果沒(méi)有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 {
        // 沒(méi)有獲得上次更新時(shí)間(應(yīng)該是第一次更新或者多次更新,之前的更新都失敗了)
        self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@",
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];
    }
}

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

  1. 作者通過(guò)使用block來(lái)讓用戶(hù)自己定義日期現(xiàn)實(shí)的格式,如果用戶(hù)沒(méi)有自定義,就使用作者提供的默認(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的樣式。

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

兩個(gè)view

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

同MJRefreshStateHeader一樣,也是重寫(xiě)了父類(lèi)的三個(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,這樣一來(lái)就不會(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īng)顟B(tài)中切換過(guò)來(lá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;
                //菊花停止旋轉(zhuǎn)
                self.loadingView.alpha = 1.0;
                [self.loadingView stopAnimating];
                //顯示箭頭
                self.arrowView.hidden = NO;
            }];
            
        } else {
            //1.2 從其他狀態(tài)中切換過(guò)來(lá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īng)顟B(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īng)顟B(tài)
        self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動(dòng)畫(huà)完畢動(dòng)作沒(méi)有被執(zhí)行
        //菊花旋轉(zhuǎn)
        [self.loadingView startAnimating];
        //隱藏arrowView
        self.arrowView.hidden = YES;
    }
}

到此為止,我們已經(jīng)從MJRefreshComponentMJRefreshNormalHeader的實(shí)現(xiàn)過(guò)程看了一遍??梢钥闯觯髡邔?code>prepare,placeSubviews以及setState:方法作為基類(lèi)的方法,讓下面的子類(lèi)去一層一層實(shí)現(xiàn)。

而每一層的子類(lèi),根據(jù)自身的職責(zé),分別按照自己的方式來(lái)實(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)下的顯示。

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

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

還是提供一張圖來(lái)直觀感受一下:


normalHeader 與 gifHeader

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

MJRefreshGifHeader

它提供了兩個(gè)接口,是用來(lái)設(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 
{ 
   //如果沒(méi)有傳入duration,則根據(jù)圖片的多少來(lái)計(jì)算
    [self setImages:images duration:images.count * 0.1 forState:state]; 
}

有哪些職能?

然后,和MJRefreshNormalHeader一樣,它也重寫(xiě)了基類(lèi)提供的三個(gè)方法來(lái)實(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)的不同來(lái)設(shè)置動(dòng)畫(huà)

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {
        
        //1. 如果傳進(jìn)來(lái)的狀態(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)來(lái)的狀態(tài)是默認(rèn)狀態(tài)
        [self.gifView stopAnimating];
    }
}

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

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

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

歡迎來(lái)參觀 ^^

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

獲取授權(quán)

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

注意注意?。。?/strong>

筆者在近期開(kāi)通了個(gè)人公眾號(hào),主要分享編程,讀書(shū)筆記,思考類(lèi)的文章。

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

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

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

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

公眾號(hào):程序員維他命
最后編輯于
?著作權(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ù)。

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

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

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