iOS源碼解析之--MJRefresh

博主寫的特別棒宁舰,在此收藏一遍以防丟失,供自己學(xué)習(xí)參考之用奢浑。鏈接如下:
MJRefresh源碼解析

MJRefresh是李明杰老師的作品明吩,到現(xiàn)在已經(jīng)有9800多顆star了,是一個簡單實用殷费,功能強(qiáng)大的iOS下拉刷新(也支持上拉加載更多)控件印荔。它的可定制性很高,幾乎可以滿足大部分下拉刷新的設(shè)計需求详羡,值得學(xué)習(xí)仍律。

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

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

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

MJRefreshComponent

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

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

職能如何實現(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)(void);
/** 開始刷新后的回調(diào)(進(jìn)入刷新狀態(tài)后的回調(diào)) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)(void);
/** 結(jié)束刷新后的回調(diào) */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)(void);

3. 添加監(jiān)聽

監(jiān)聽的聲明:

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

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

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

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

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

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

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

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

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

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

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

MJRefreshHeader

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

有哪些職能葱她?

  1. 初始化。
  2. 設(shè)置header高度似扔。
  3. 重新調(diào)整y值吨些。
  4. 根據(jù)contentOffset的變化,來切換狀態(tài)(默認(rèn)狀態(tài)炒辉,可以刷新的狀態(tài)豪墅,正在刷新的狀態(tài)),實現(xiàn)方法是:scrollViewContentOffsetDidChange:黔寇。
  5. 在切換狀態(tài)時偶器,執(zhí)行相應(yīng)的操作。實現(xiàn)方法是:setState:缝裤。

職能如何實現(xiàn)屏轰?

1. 初始化

初始化有兩種方法:

+ (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. 設(shè)置header高度

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

- (void)prepare
{
    [super prepare];
    
    // 設(shè)置key
    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
    
    // 設(shè)置高度
    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;
}

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

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    // 在刷新的refreshing狀態(tài)
    if (self.state == MJRefreshStateRefreshing) {
//        if (self.window == nil) return;
        
        // sectionheader停留解決
        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)到下一個控制器時霎苗,contentInset可能會變
     _scrollViewOriginalInset = self.scrollView.mj_inset;
    
    // 當(dāng)前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 頭部控件剛好出現(xiàn)的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 如果是向上滾動到看不見頭部控件,直接返回
    // >= -> >
    if (offsetY > happenOffsetY) return;
    
    // 普通 和 即將刷新 的臨界點
    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) {
            // 轉(zhuǎn)為即將刷新狀態(tài)
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
            // 轉(zhuǎn)為普通狀態(tài)
            self.state = MJRefreshStateIdle;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
        // 開始刷新
        [self beginRefreshing];
    } else if (pullingPercent < 1) {
        self.pullingPercent = pullingPercent;
    }
}

需要注意三點:

  1. 這里的狀態(tài)有三種:默認(rèn)狀態(tài)(MJRefreshStateIdle)榛做,可以刷新的狀態(tài)(MJRefreshStatePulling)以及正在刷新的狀態(tài)(MJRefreshStateRefreshing)唁盏。
  2. 狀態(tài)切換的因素有兩個:一個是下拉的距離是否超過臨界值,另一個是 手指是否離開屏幕检眯。
  3. 注意:可以刷新的狀態(tài)和正在刷新的狀態(tài)是不同的厘擂。因為在手指還貼在屏幕的時候是不能進(jìn)行刷新的。所以即使在下拉的距離超過了臨界距離(狀態(tài)欄 + 導(dǎo)航欄 + header高度)锰瘸,如果手指沒有離開屏幕刽严,那么也不能馬上進(jìn)行刷新,而是將狀態(tài)切換為:可以刷新获茬。一旦手指離開了屏幕港庄,馬上將狀態(tài)切換為正在刷新。

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


三個狀態(tài).png

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

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStateIdle) {
        if (oldState != MJRefreshStateRefreshing) return;
        
        // 保存刷新時間
        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
        // 恢復(fù)inset和offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            self.scrollView.mj_insetT += self.insetTDelta;
            
            // 自動調(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;
                // 增加滾動區(qū)域top
                self.scrollView.mj_insetT = top;
                // 設(shè)置滾動位置
                CGPoint offset = self.scrollView.contentOffset;
                offset.y = -top;
                [self.scrollView setContentOffset:offset animated:NO];
            } completion:^(BOOL finished) {
                [self executeRefreshingCallback];
            }];
         });
    }
}

這里需要注意兩點:

  1. 這里狀態(tài)的切換恕曲,主要圍繞著兩種:默認(rèn)狀態(tài)和正在刷新狀態(tài)鹏氧。也就是針對開始刷新和結(jié)束刷新這兩個切換點。
  2. 從正在刷新狀態(tài)狀態(tài)切換為默認(rèn)狀態(tài)時(結(jié)束刷新)佩谣,需要記錄刷新結(jié)束的時間把还。因為header里面有一個默認(rèn)的label是用來顯示上次刷新的時間的。

MJRefreshStateHeader

這個類是MJRefreshHeader類的子類茸俭,它做了兩件事:

有哪些職能吊履?

  1. 簡單布局了stateLabellastUpdatedTimeLabel
  2. 根據(jù)控件狀態(tài)的切換(默認(rèn)狀態(tài)调鬓,正在刷新狀態(tài))艇炎,實現(xiàn)了這兩個label顯示的文字的切換。
    給一張圖腾窝,讓大家直觀感受一下這兩個控件:


    兩個Label.png

職能如何實現(xiàn)缀踪?

這個類通過覆蓋父類三個方法來實現(xiàn)上述兩個實現(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];
}

在這里,將每一個狀態(tài)對應(yīng)的提示文字放入一個字典里面,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) {
        // 狀態(tài)
        if (noConstrainsOnStatusLabel) self.stateLabel.frame = self.bounds;
    } else {
        CGFloat stateLabelH = self.mj_h * 0.5;
        // 狀態(tài)
        if (noConstrainsOnStatusLabel) {
            self.stateLabel.mj_x = 0;
            self.stateLabel.mj_y = 0;
            self.stateLabel.mj_w = self.mj_w;
            self.stateLabel.mj_h = stateLabelH;
        }
        
        // 更新時間
        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;
        }
    }
}

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

方法3: setState:方法

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

在這里循集,根據(jù)傳入的state的不同唇敞,在stateLabellastUpdatedTimeLabel里切換相應(yīng)的文字。

  • stateLabel里的文字直接從stateTitles字典里取出即可咒彤。
    < - lastUpdatedTimeLabel里的文字需要通過一個方法來取出即可疆柔。
- (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";
            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 {
        self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@",
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
                                          [NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];
    }
}

在這里注意兩點:

  1. 作者通過使用block來讓用戶自己定義日期現(xiàn)實的格式镶柱,如果用戶沒有自定義婆硬,就使用作者提供的默認(rèn)格式。
  2. 在默認(rèn)格式的設(shè)置里奸例,判斷了是否是今日彬犯,是否是今年的情況。在以后設(shè)計顯示時間的labe的時候可以借鑒一下查吊。

MJRefreshNormalHeader

MJRefreshNormalHeader 繼承于 MJRefreshStateHeader谐区,它主要做了兩件事:

有哪些職能?

  1. 它在MJRefreshStateHeader上添加了_arrowViewloadingView逻卖。
  2. 布局了這兩個view并在Refresh控件的狀態(tài)切換的時候改變這兩個view的樣式宋列。
    還是給一張圖來直觀感受一下這兩個view:


    兩個view.png

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

同MJRefreshStateHeader一樣评也,也是重寫了父類的三個方法:

方法1:prepare

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

方法2:placeSubviews

- (void)placeSubviews
{
    [super placeSubviews];
    
    // 箭頭的中心點
    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;
        }
        CGFloat textWidth = MAX(stateWidth, timeWidth);
        arrowCenterX -= textWidth / 2 + self.labelLeftInset;
    }
    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;
        self.arrowView.center = arrowCenter;
    }
        
    // 圈圈
    if (self.loadingView.constraints.count == 0) {
        self.loadingView.center = arrowCenter;
    }
    
    self.arrowView.tintColor = self.stateLabel.textColor;
}

在這里注意一點:因為stateLabellastUpdatedTimeLabel是上下并排分布的炼杖,而arrowViewloadingView是在這二者的左邊灭返,所以為了避免這兩組重合,在計算arrowViewloadingViewcenter的時候坤邪,需要獲取stateLabellastUpdatedTimeLabel兩個控件的寬度并比較大小熙含,將較大的一個作為兩個label的‘最寬距離’,再計算center艇纺,這樣一來就不會重合了怎静。
而對于如何計算寬度,作者給出了一個方案黔衡,大家可以在以后的實踐中使用蚓聘。

- (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)做事情
    if (state == MJRefreshStateIdle) {
        if (oldState == MJRefreshStateRefreshing) {
            self.arrowView.transform = CGAffineTransformIdentity;
            
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                self.loadingView.alpha = 0.0;
            } completion:^(BOOL finished) {
                // 如果執(zhí)行完動畫發(fā)現(xiàn)不是idle狀態(tài),就直接返回盟劫,進(jìn)入其他狀態(tài)
                if (self.state != MJRefreshStateIdle) return;
                
                self.loadingView.alpha = 1.0;
                [self.loadingView stopAnimating];
                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);
        }];
    } else if (state == MJRefreshStateRefreshing) {
        self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動畫完畢動作沒有被執(zhí)行
        [self.loadingView startAnimating];
        self.arrowView.hidden = YES;
    }
}

到此為止夜牡,我們已經(jīng)從MJRefreshComponentMJRefreshNormalHeader的實現(xiàn)過程看了一遍÷虑可以看出氯材,作者將prepareplaceSubviews以及setState:方法作為基類的方法硝岗,讓下面的子類去一層一層實現(xiàn)氢哮。

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

  • 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屬于同一級,都是繼承于MJRefreshStateHeader无埃。因為二者都具有相同形式的stateLabellastUpdatedTimeLabel徙瓶,唯一不同的就是左側(cè)的部分:

  • MJRefreshNormalHeader的左側(cè)是箭頭。
  • MJRefreshGifHeader的左側(cè)則是一個gif動畫嫉称。
    還是提供一張圖來直觀感受一下:
    normalHeader 與 gifHeader.png

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

MJRefreshGifHeader

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

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

有哪些職能?

然后织阅,和MJRefreshNormalHeader一樣壳繁,它也重寫了基類提供的三個方法來實現(xiàn)顯示gif圖片的職能。

職能如何實現(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) {
        self.gifView.contentMode = UIViewContentModeCenter;
    } else {
        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è)置動畫

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根據(jù)狀態(tài)做事情
    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];
    }
}

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市壁查,隨后出現(xiàn)的幾起案子觉至,更是在濱河造成了極大的恐慌,老刑警劉巖睡腿,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件语御,死亡現(xiàn)場離奇詭異,居然都是意外死亡席怪,警方通過查閱死者的電腦和手機(jī)应闯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挂捻,“玉大人碉纺,你說我怎么就攤上這事】倘觯” “怎么了骨田?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長声怔。 經(jīng)常有香客問我态贤,道長,這世上最難降的妖魔是什么醋火? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任悠汽,我火速辦了婚禮,結(jié)果婚禮上芥驳,老公的妹妹穿的比我還像新娘介粘。我一直安慰自己,他們只是感情好晚树,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布姻采。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪慨亲。 梳的紋絲不亂的頭發(fā)上婚瓜,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機(jī)與錄音刑棵,去河邊找鬼巴刻。 笑死,一個胖子當(dāng)著我的面吹牛蛉签,可吹牛的內(nèi)容都是我干的胡陪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼碍舍,長吁一口氣:“原來是場噩夢啊……” “哼柠座!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起片橡,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤妈经,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后捧书,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吹泡,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年经瓷,在試婚紗的時候發(fā)現(xiàn)自己被綠了爆哑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡舆吮,死狀恐怖揭朝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情歪泳,我是刑警寧澤萝勤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站呐伞,受9級特大地震影響敌卓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜伶氢,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一趟径、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧癣防,春花似錦蜗巧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春望拖,著一層夾襖步出監(jiān)牢的瞬間渺尘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工说敏, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留鸥跟,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓盔沫,卻偏偏與公主長得像医咨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子架诞,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

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

  • MJRefresh 是著名開發(fā)者及培訓(xùn)講師李明杰老師的作品拟淮,到現(xiàn)在在github已經(jīng)有10000多顆star,真真...
    藍(lán)色小石頭閱讀 4,393評論 6 33
  • MJRefresh在iOS中是一個簡單實用功能強(qiáng)大的刷新的控件侈贷〕颓福可定制很高等脂,幾乎可以滿足大部分的App對刷新控件的...
    charleswang閱讀 558評論 0 2
  • 簡書博客已經(jīng)暫停更新俏蛮,想看更多技術(shù)博客請到: 掘金 :J_Knight_ 個人博客: J_Knight_ 個人公眾...
    J_Knight_閱讀 10,940評論 31 131
  • 本文轉(zhuǎn)載自J_Knight 的MJRefresh源碼解析 MJRefresh是李明杰的作品,到現(xiàn)在已經(jīng)有9800多...
    Detective41閱讀 660評論 0 1
  • MJRefresh是李明杰老師的作品上遥,對于iOS的開發(fā)者來說搏屑,一定非常熟悉這個簡單實用,功能強(qiáng)大的下拉加載粉楚,上拉刷...
    CerasusLand閱讀 669評論 0 1