iOS源碼閱讀 —— MJRefresh

前言:最近應(yīng)該有很多小伙伴去跳槽面試的吧,相信各位有的已經(jīng)順利收到offer了季惩,而有些則是碰壁了录粱,那么我在這里給大家準(zhǔn)備了相關(guān)面試資料腻格,還有相關(guān)算法資料。想了解的可找我拿

MJRefresh幾乎是我們開(kāi)發(fā)工作中必用的一款三方庫(kù)啥繁,它提供一套非常簡(jiǎn)單實(shí)用的拖拽執(zhí)行回調(diào)事件的解決方案菜职。下面是官方提供的框架圖。

其中最常用的幾個(gè)默認(rèn)視圖類(lèi)分別是:

下拉刷新控件:MJRefreshNormalHeader
上拉加載控件:MJRefreshAutoNormalFooter酬核、MJRefreshBackNormalFooter
左滑加載控件:MJRefreshNormalTrailer

下面將對(duì)這些類(lèi),自上而下地進(jìn)行分析宪睹。

公共基類(lèi)控件

MJRefreshComponent

通過(guò)框架圖可以看出所有視圖都源于同一個(gè)基類(lèi)——MJRefreshComponent愁茁,它為子類(lèi)提供了公用的屬性和事件,主要有:

  • 回調(diào)對(duì)象和回調(diào)方法
  • 拖拽狀態(tài)定義和控制
  • 通過(guò)KVO亭病,對(duì)事件(控件偏移鹅很、內(nèi)容尺寸、手勢(shì)狀態(tài))添加監(jiān)聽(tīng)(回調(diào)響應(yīng)交給子類(lèi)實(shí)現(xiàn))
  • 其他:
    • 拖拽百分比
    • 根據(jù)拖拽比例自動(dòng)切換透明度

MJRefreshComponent還為子類(lèi)搭建了基本的邏輯框架:

最后推薦個(gè)我的iOS交流圈:[891 488 181]
'有一個(gè)共同的圈子很重要罪帖,結(jié)識(shí)人脈促煮!里面都是iOS開(kāi)發(fā),全棧發(fā)展整袁,歡迎入駐菠齿,共同進(jìn)步!(群內(nèi)會(huì)免費(fèi)提供一些群主收藏的免費(fèi)學(xué)習(xí)書(shū)籍資料以及整理好的幾百道面試題和答案文檔W肌)

視圖創(chuàng)建

// 1.初始化
- (instancetype)initWithFrame:(CGRect)frame{;}

// 2.準(zhǔn)備工作
- (void)prepare{;}

// 3.視圖即將被父視圖加入
- (void)willMoveToSuperview:(UIView *)newSuperview{
    // 滾動(dòng)視圖初始值的記錄
    // 一些值的更新
    // 監(jiān)聽(tīng)事件的更新
}

// 4.布局
- (void)layoutSubviews{
    [self placeSubviews];
}

滾動(dòng)視圖狀態(tài)回調(diào)

// 當(dāng)偏移值發(fā)生變化
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
// 當(dāng)內(nèi)容大小發(fā)生變化
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
// 當(dāng)點(diǎn)擊手勢(shì)狀態(tài)發(fā)生變化
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}

狀態(tài)設(shè)置

// 狀態(tài)設(shè)置
- (void)setState:(MJRefreshState)state{;}

常用方法

// 進(jìn)入刷新?tīng)顟B(tài)
- (void)beginRefreshing{;}
// 結(jié)束刷新?tīng)顟B(tài)
- (void)endRefreshing{;}

其他

// 自動(dòng)切換透明度
- (void)setAutoChangeAlpha:(BOOL)autoChangeAlpha{;}
- (BOOL)isAutoChangeAlpha{;}
- (void)setAutomaticallyChangeAlpha:(BOOL)automaticallyChangeAlpha{;}

// 根據(jù)拖拽進(jìn)度實(shí)時(shí)設(shè)置透明度
- (void)setPullingPercent:(CGFloat)pullingPercent{;}

下拉刷新控件(Header)

下拉刷新控件包含四個(gè)類(lèi):

  • MJRefreshHeader
    • MJRefreshStateHeader
      • MJRefreshNormalHeader
    • MJRefreshGifHeader

MJRefreshHeader

MJRefreshHeader類(lèi)是一個(gè)包含了完整的下拉刷新功能邏輯的空白視圖绳匀,子類(lèi)MJRefreshStateHeaderMJRefreshGifHeader只需要再添加一些額外的圖片和文字,就能提升使用體驗(yàn)和保持代碼的簡(jiǎn)潔易讀性炸客。

實(shí)現(xiàn)過(guò)程

1.初始化

創(chuàng)建視圖疾棵,設(shè)置高度和位置。

- (void)prepare {
    [super prepare];
    // 設(shè)置存儲(chǔ)key
    // 設(shè)置Header的高度
}

- (void)placeSubviews {
    [super placeSubviews];    
    // 設(shè)置Header的位置(y坐標(biāo))
}
2.偏移變化:- scrollViewContentSizeDidChange

當(dāng)用戶拖拽滾動(dòng)控件痹仙,是其偏移值發(fā)生改變時(shí)是尔,會(huì)回調(diào)- scrollViewContentOffsetDidChange:(NSDictionary *)change方法,在不同的狀態(tài)下執(zhí)行對(duì)應(yīng)的邏輯开仰。如果滾動(dòng)視圖已經(jīng)將Header滾動(dòng)至屏幕外拟枚,則不處理后續(xù)邏輯。

- scrollViewContentOffsetDidChange:(NSDictionary *)change方法中众弓,有一些關(guān)鍵的變量值恩溅,分別是:

  • 當(dāng)前滾動(dòng)的偏移值:offsetY
  • 頭部控件剛好出現(xiàn)的偏移值:happenOffsetY
  • 即將刷新的臨界點(diǎn):normal2pullingOffsetY

通過(guò)對(duì)這些變量值的比較,可以計(jì)算出拖拽動(dòng)作應(yīng)該被設(shè)置為何種狀態(tài)谓娃。

  • 控件正在被拖拽
    • 當(dāng)拖拽時(shí)的偏移量大于臨界值脚乡,且原狀態(tài)為閑置時(shí),將狀態(tài)置為即將刷新
    • 當(dāng)拖拽時(shí)的偏移量小于臨界值傻粘,且原狀態(tài)為即將刷新時(shí)每窖,將狀態(tài)重置會(huì)閑置
  • 控件未被拖拽,且當(dāng)前狀態(tài)為松手進(jìn)行刷新
    • 執(zhí)行開(kāi)始刷新的方法
  • 控件未被拖拽弦悉,且為達(dá)到執(zhí)行刷新回調(diào)的臨界點(diǎn)
if (self.scrollView.isDragging) { // 如果正在拖拽
    self.pullingPercent = pullingPercent;

    // 當(dāng)拖拽時(shí)的偏移量大于臨界值窒典,且原狀態(tài)為閑置時(shí),將狀態(tài)置為即將刷新
    if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
        // 轉(zhuǎn)為即將刷新?tīng)顟B(tài)
        self.state = MJRefreshStatePulling;
    }
    // 當(dāng)拖拽時(shí)的偏移量小于臨界值稽莉,且原狀態(tài)為即將刷新時(shí)瀑志,將狀態(tài)重置會(huì)閑置
    else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
        // 轉(zhuǎn)為普通狀態(tài)
        self.state = MJRefreshStateIdle;
    }
}
// 原狀態(tài)為即將刷新,且手已松開(kāi)
else if (self.state == MJRefreshStatePulling) {
    // 開(kāi)始刷新
    [self beginRefreshing];
}
// 未達(dá)到刷新的偏移量污秆,且手已松開(kāi)
else if (pullingPercent < 1) {
    // 記錄header露出的百分比
    self.pullingPercent = pullingPercent;
}

這里需要注意的是劈猪,當(dāng)Header的狀態(tài)處于MJRefreshStateRefreshing正在刷新,且控件還在滾動(dòng)時(shí)良拼,會(huì)執(zhí)行- resetInset方法战得,目的是記錄刷新結(jié)束后需要調(diào)整的上邊距值insetTDelta,同時(shí)避免 CollectionView 在使用根據(jù) Autolayout 和 內(nèi)容自動(dòng)伸縮 Cell, 刷新時(shí)導(dǎo)致的 Layout 異常渲染問(wèn)題庸推。

3.狀態(tài)設(shè)置
- (void)setState:(MJRefreshState)state{
    _state = state;

    // 加入主隊(duì)列的目的是等setState:方法調(diào)用完畢常侦、設(shè)置完文字后再去布局子控件
    MJRefreshDispatchAsyncOnMainQueue([self setNeedsLayout];)
}

視圖刷新被加入了異步隊(duì)列的主線程中,是為了盡量等空間的屬性設(shè)置完畢后再進(jìn)行布局的刷新贬媒。

4.開(kāi)始刷新

執(zhí)行- beginRefreshing方法聋亡,設(shè)置狀態(tài)為MJRefreshStateRefreshing刷新中。 方法調(diào)用流程如下:

1.開(kāi)始刷新方法調(diào)用
- (void)beginRefreshing{
   // ...
   self.state = MJRefreshStateRefreshing;
   // ...
}

2.設(shè)置狀態(tài)為正在刷新中
- (void)setState:(MJRefreshState)state{
    MJRefreshCheckState

    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStateIdle) {
        //...        
    } else if (state == MJRefreshStateRefreshing) {
        [self headerRefreshingAction];
    }
}
3.執(zhí)行刷新動(dòng)作
- (void)headerRefreshingAction {
    // 主要代碼
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
        if (self.scrollView.panGestureRecognizer.state != UIGestureRecognizerStateCancelled) {
            CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
            // 增加滾動(dòng)區(qū)域top
            self.scrollView.mj_insetT = top;
            // 設(shè)置滾動(dòng)位置
            CGPoint offset = self.scrollView.contentOffset;
            offset.y = -top;
            [self.scrollView setContentOffset:offset animated:NO];
        }
    } completion:^(BOOL finished) {
        [self executeRefreshingCallback];
    }];
}

- headerRefreshingAction方法為滾動(dòng)視圖設(shè)置了新的insetoffset际乘,使得Header能在滾動(dòng)視圖的頂部停留坡倔,用于展示刷新文字動(dòng)畫(huà)之類(lèi)的。

5.結(jié)束刷新

結(jié)束刷新需要使用者在耗時(shí)操作結(jié)束后脖含,主動(dòng)調(diào)用- endRefreshing方法罪塔。 方法調(diào)用流程如下:

1.結(jié)束刷新方法調(diào)用
- (void)endRefreshing{
    MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateIdle;)
}
2.設(shè)置狀態(tài)為閑置
- (void)setState:(MJRefreshState)state{
    MJRefreshCheckState

    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStateIdle) {
        if (oldState != MJRefreshStateRefreshing) return;

        [self headerEndingAction];
    } else if (state == MJRefreshStateRefreshing) {
        // ...
    }
}
3.執(zhí)行結(jié)束動(dòng)作
- (void)headerEndingAction {;}

- headerEndingAction方法將滾動(dòng)視圖的inset重置為刷新?tīng)顟B(tài)前的值,將header又隱藏了起來(lái)

上拉加載控件(Footer)

下拉刷新控件包含七個(gè)類(lèi):

  • MJRefreshFooter
    • MJRefreshBackFooter
      • MJRefreshBackNormalFooter
      • MJRefreshBackGifFooter
    • MJRefreshAutoFooter
      • MJRefreshAutoNormalFooter
      • MJRefreshAutoGifFooter

MJRefreshFooter

MJRefreshFooter類(lèi)不能直接被使用器赞,它僅定義了少量的基礎(chǔ)屬性和方法垢袱,例如構(gòu)造方法、初始化控件高度港柜,以及無(wú)數(shù)據(jù)加載情況下的處理请契。

能夠直接使用的上拉加載控件是,由MJRefreshFooter衍生出的兩個(gè)子類(lèi)夏醉,MJRefreshBackFooterMJRefreshAutoFooter爽锥,這兩個(gè)控件的不同之處在于:

  • MJRefreshBackFooter:隱藏在滾動(dòng)視圖的底部邊界之外,當(dāng)拖動(dòng)至Footer的刷新臨界點(diǎn)并放開(kāi)手畔柔,才會(huì)執(zhí)行加載操作氯夷。
  • MJRefreshAutoFooter:會(huì)緊貼在滾動(dòng)視圖contentSize的邊界,如果contentSize的尺寸小于滾動(dòng)視圖的尺寸靶擦,用戶在不需要滾動(dòng)的情況下也能看到Footer控件的腮考。它的刷新時(shí)機(jī)是雇毫,用戶在拖拽中且達(dá)到了Footer刷新臨界點(diǎn)。

MJRefreshBackFooter

實(shí)現(xiàn)過(guò)程

1.初始化

當(dāng)MJRefreshBackFooter即將被加入父視圖時(shí)踩蔚,會(huì)走 - willMoveToSuperview: 方法棚放,并在方法體內(nèi)調(diào)用 - scrollViewContentSizeDidChange: 方法。該方法獲取了父視圖高度和父視圖內(nèi)容的高度馅闽,取二者中較大的數(shù)飘蚯,作為Footer的縱坐標(biāo)值,確保Footer的位置正好隱藏在視圖或內(nèi)容的最底部福也。

- (void)scrollViewContentSizeDidChange:(NSDictionary *)change
{
    [super scrollViewContentSizeDidChange:change];

    // 內(nèi)容的高度
    CGFloat contentHeight = self.scrollView.mj_contentH + self.ignoredScrollViewContentInsetBottom;
    // 表格的高度
    CGFloat scrollHeight = self.scrollView.mj_h - self.scrollViewOriginalInset.top - self.scrollViewOriginalInset.bottom + self.ignoredScrollViewContentInsetBottom;
    // 設(shè)置位置和尺寸
    self.mj_y = MAX(contentHeight, scrollHeight);
}
2.偏移變化:- scrollViewContentSizeDidChange

當(dāng)用戶在滑動(dòng)控件使offset發(fā)生變化時(shí)局骤,會(huì)觸發(fā) MJRefreshKeyPathContentOffset 的監(jiān)聽(tīng)事件 —— - scrollViewContentOffsetDidChangeMJRefreshBackFooter- scrollViewContentOffsetDidChange方法里的代碼邏輯與MJRefreshHeader是幾乎相同的暴凑,這里不贅述峦甩。需要提一點(diǎn)的是,MJRefreshBackFooter視圖的臨界值計(jì)算需要考慮內(nèi)容的高度與滾動(dòng)視圖之間的高度差問(wèn)題搬设。

#pragma mark 獲得scrollView的內(nèi)容 超出 view 的高度
- (CGFloat)heightForContentBreakView
{
    CGFloat h = self.scrollView.frame.size.height - self.scrollViewOriginalInset.bottom - self.scrollViewOriginalInset.top;
    return self.scrollView.contentSize.height - h;
}

#pragma mark 剛好看到上拉刷新控件時(shí)的contentOffset.y
- (CGFloat)happenOffsetY
{
    CGFloat deltaH = [self heightForContentBreakView];
    // 內(nèi)容和視圖的高度差
    if (deltaH > 0) {
        //  內(nèi)容高度 > 視圖高度
        return deltaH - self.scrollViewOriginalInset.top;
    } else {
        // 內(nèi)容高度 < 視圖高度
        return - self.scrollViewOriginalInset.top;
    }
}
3.狀態(tài)設(shè)置

MJRefreshBackFooter類(lèi)的- setState方法的主要工作就是在開(kāi)始刷新和結(jié)束刷新的時(shí)候穴店,為滾動(dòng)視圖更新對(duì)應(yīng)的offsetinset值。

MJRefreshAutoFooter

實(shí)現(xiàn)過(guò)程

1.初始化

當(dāng)MJRefreshAutoFooter即將被加入父視圖時(shí)拿穴,會(huì)調(diào)用- willMoveToSuperview: 方法泣洞,該方法獲取了父視圖的內(nèi)容,作為Footer的縱坐標(biāo)y的值默色,確保Footer的位置正好緊貼內(nèi)容的底部球凰。

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];

    if (newSuperview) { // 新的父控件
        if (self.hidden == NO) {
            self.scrollView.mj_insetB += self.mj_h;
        }

        // 設(shè)置位置
        self.mj_y = _scrollView.mj_contentH;
    } else { // 被移除了
        if (self.hidden == NO) {
            self.scrollView.mj_insetB -= self.mj_h;
        }
    }
}
2.刷新邏輯

MJRefreshAutoFooter控件的位置是緊貼內(nèi)容的,所以會(huì)存在兩種情況: 1.當(dāng)內(nèi)容高度 < 控件高度腿宰,可以直接看到緊貼內(nèi)容底部的Footer呕诉。這種情況下,加載的時(shí)機(jī)是在用戶松手后調(diào)用的吃度。

- (void)scrollViewPanStateDidChange:(NSDictionary *)change
{
    [super scrollViewPanStateDidChange:change];

    if (self.state != MJRefreshStateIdle) return;

    UIGestureRecognizerState panState = _scrollView.panGestureRecognizer.state;

    switch (panState) {
        // 手松開(kāi)
        case UIGestureRecognizerStateEnded: {
            if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h) {
                // 內(nèi)容 < 控件高度
                if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { // 向上拽
                    self.triggerByDrag = YES;
                    [self beginRefreshing];
                }
            } else {
                // 內(nèi)容 > 控件高度
                if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView.mj_insetB - _scrollView.mj_h) {
                    self.triggerByDrag = YES;
                    [self beginRefreshing];
                }
            }
        }
            break;

        case UIGestureRecognizerStateBegan: {
            [self resetTriggerTimes];
        }
            break;

        default:
            break;
    }
}

2.當(dāng)內(nèi)容高度 ≥ 控件高度甩挫,需要拖動(dòng)視圖到Footer的加載臨界值,但此時(shí)不需要松開(kāi)手椿每,只要滾動(dòng)視圖的偏移量突破了臨界值伊者,就會(huì)觸發(fā)加載方法。

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];

    if (self.state != MJRefreshStateIdle || !self.automaticallyRefresh || self.mj_y == 0) return;

    // 當(dāng)autoTriggerTimes被設(shè)置成-1(滾動(dòng)時(shí)無(wú)限加載)
    // 該方法保證拖動(dòng)放手后间护,視圖還在滾動(dòng)的情況下亦渗,一直保持加載狀態(tài)

    // 內(nèi)容超出控件高度
    if (_scrollView.mj_insetT + _scrollView.mj_contentH > _scrollView.mj_h) {

        //  內(nèi)容高度 - 控件高度 + 控件底部邊距 + footer高度 * 百分比 - footer高度
        //  內(nèi)容高度 - 控件高度 + 控件底部邊距
        if (_scrollView.mj_offsetY >= _scrollView.mj_contentH - _scrollView.mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + _scrollView.mj_insetB - self.mj_h) {
            // 防止手松開(kāi)時(shí)連續(xù)調(diào)用
            CGPoint old = [change[@"old"] CGPointValue];
            CGPoint new = [change[@"new"] CGPointValue];
            if (new.y <= old.y) return;

            if (_scrollView.isDragging) {
                self.triggerByDrag = YES;
            }
            // 當(dāng)?shù)撞克⑿驴丶耆霈F(xiàn)時(shí),才刷新
            [self beginRefreshing];
        }
    }
}

從代碼中可以看出汁尺,滿足刷新的條件是: 拖動(dòng)偏移量 ≥ 內(nèi)容高度 - 控件高度 + 控件底部邊距 + footer高度 * 刷新控件露出百分比 - footer高度 當(dāng)刷新控件露出百分比為默認(rèn)值1.時(shí)法精,不等式可以簡(jiǎn)化為: 拖動(dòng)偏移量 ≥ 內(nèi)容高度 - 控件高度 + 控件底部邊距

3.無(wú)限觸發(fā)

MJRefreshAutoFooter的另一特點(diǎn)就是無(wú)限觸發(fā),開(kāi)發(fā)者可以設(shè)置屬性自定義自動(dòng)刷新的次數(shù)。

/** 自動(dòng)觸發(fā)次數(shù), 默認(rèn)為 1, 僅在拖拽 ScrollView 時(shí)才生效,

 如果為 -1, 則為無(wú)限觸發(fā)
 */
@property (nonatomic) NSInteger autoTriggerTimes;

當(dāng)滾動(dòng)視圖在持續(xù)地滾動(dòng)時(shí)(內(nèi)容高度≥控件高度)搂蜓,會(huì)不停地調(diào)用-scrollViewContentOffsetDidChange:方法狼荞,滿足加載條件時(shí)從而不停的調(diào)用-beginRefreshing方法。

- (void)beginRefreshing{
    // 新的拖拽動(dòng)作 && 剩余觸發(fā)次數(shù) && 是否無(wú)限觸發(fā)
    if (self.triggerByDrag && self.leftTriggerTimes <= 0 && !self.unlimitedTrigger) {
        return;
    }

    [super beginRefreshing];
}

當(dāng)前如果支持無(wú)限觸發(fā)autoTriggerTimes == -1帮碰,那么在滾動(dòng)視圖停止?jié)L動(dòng)前粘秆,視圖到達(dá)加載臨界點(diǎn)時(shí)都會(huì)觸發(fā)加載任務(wù)。

4.狀態(tài)設(shè)置

-beginRefreshing在觸發(fā)的時(shí)候收毫,會(huì)將state設(shè)置成MJRefreshStateRefreshing,并執(zhí)行加載數(shù)據(jù)的回調(diào)殷勘。 通常我們會(huì)在加載數(shù)據(jù)結(jié)束的回調(diào)方法中去調(diào)用-endRefreshing-endRefreshingWithNoMoreData方法此再,此時(shí)state會(huì)被設(shè)置成MJRefreshStateIdleMJRefreshStateNoMoreData,對(duì)應(yīng)-setState:方法中的代碼玲销,我們可以看到無(wú)限觸發(fā)次數(shù)是在此處進(jìn)行了控制输拇。

- (void)setState:(MJRefreshState)state{
    MJRefreshCheckState

    if (state == MJRefreshStateRefreshing) {
        // 執(zhí)行加載數(shù)據(jù)回調(diào)
        [self executeRefreshingCallback];
    } else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
        if (self.triggerByDrag) {
            if (!self.unlimitedTrigger) {
                self.leftTriggerTimes -= 1;
            }
            self.triggerByDrag = NO;
        }

        /** 結(jié)束刷新 */
        if (MJRefreshStateRefreshing == oldState) {

            // 當(dāng)視圖開(kāi)啟了分頁(yè)顯示,設(shè)置動(dòng)畫(huà)和回調(diào)
            if (self.scrollView.pagingEnabled) {
                CGPoint offset = self.scrollView.contentOffset;
                offset.y -= self.scrollView.mj_insetB;
                [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                    self.scrollView.contentOffset = offset;

                    if (self.endRefreshingAnimationBeginAction) {
                        self.endRefreshingAnimationBeginAction();
                    }
                } completion:^(BOOL finished) {
                    if (self.endRefreshingCompletionBlock) {
                        self.endRefreshingCompletionBlock();
                    }
                }];

                return;
            }

            // 結(jié)束刷新回調(diào)
            if (self.endRefreshingCompletionBlock) {
                self.endRefreshingCompletionBlock();
            }
        }
    }
}

左滑加載控件(Trailer)

MJRefreshTrailer 在實(shí)現(xiàn)邏輯上與 MJRefreshBackFooter 是完全一樣的贤斜,只不過(guò)是將部分參數(shù)從垂直方向換成了水平方向策吠。

State、Normal 子類(lèi)控件

State類(lèi)型的控件 的主要特點(diǎn)是添加了不同狀態(tài)的提示文字和刷新時(shí)間的顯示瘩绒。 Normal類(lèi)型的控件 在 State類(lèi)型控件 的基礎(chǔ)上猴抹,添加了箭頭圖標(biāo)和刷新的動(dòng)畫(huà)。

Gif 子類(lèi)控件

Gif類(lèi)型的控件可以在拖拽和刷新時(shí)展示精美的動(dòng)畫(huà)來(lái)提升用戶體驗(yàn)锁荔。 主要的兩個(gè)方法是:

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

刷新控件會(huì)根據(jù)圖片的高度調(diào)整自身高度蟀给,同時(shí)會(huì)在沒(méi)有自定義動(dòng)畫(huà)時(shí)長(zhǎng)的情況下,根據(jù)動(dòng)畫(huà)的幀數(shù)自動(dòng)設(shè)置完整播放一遍動(dòng)畫(huà)的時(shí)間阳堕。

拖拽動(dòng)畫(huà)

開(kāi)發(fā)者可以通過(guò)拖拽百分比設(shè)置用戶在拖拽時(shí)的動(dòng)畫(huà)跋理,具體實(shí)現(xiàn)方式是通過(guò)計(jì)算當(dāng)前拖拽的百分比在整體動(dòng)畫(huà)中對(duì)應(yīng)的某個(gè)幀的圖片來(lái)獲取大致的下標(biāo)。

// 通過(guò)拖拽百分比設(shè)置 Idle~Pulling狀態(tài)之間的 對(duì)應(yīng)的動(dòng)畫(huà)幀
- (void)setPullingPercent:(CGFloat)pullingPercent
{
    [super setPullingPercent:pullingPercent];
    NSArray *images = self.stateImages[@(MJRefreshStateIdle)];
    if (self.state != MJRefreshStateIdle || images.count == 0) return;
    // 停止動(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];
}

刷新動(dòng)畫(huà)

刷新動(dòng)畫(huà)會(huì)在視圖狀態(tài)處于“即將開(kāi)始刷新”和“刷新中”進(jìn)行恬总,使用UIImageView的startAnimating對(duì)提前設(shè)置好的圖片組逐幀播放前普,默認(rèn)情況下是無(wú)限循環(huán)播放的。

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState

    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {
        // 即將刷新 和 刷新中 狀態(tài)的動(dòng)畫(huà)
        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) {
        // 限制狀態(tài)停止動(dòng)畫(huà)
        [self.gifView stopAnimating];
    }
}

總結(jié)

MJRefresh清晰整齊的架構(gòu)為開(kāi)發(fā)者提供了及其豐富的擴(kuò)展性壹堰,而在通常沒(méi)有定制需求的情況下拭卿,默認(rèn)的控件已經(jīng)十分夠用了。

以下資料在群文件可自行下載

文章到這里就結(jié)束了缀旁,你也可以私信我及時(shí)獲取最新資料以及面試相關(guān)資料记劈。如果你有什么意見(jiàn)和建議歡迎給我留言。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末并巍,一起剝皮案震驚了整個(gè)濱河市目木,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖刽射,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件军拟,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡誓禁,警方通過(guò)查閱死者的電腦和手機(jī)懈息,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)摹恰,“玉大人辫继,你說(shuō)我怎么就攤上這事∷状龋” “怎么了姑宽?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)闺阱。 經(jīng)常有香客問(wèn)我炮车,道長(zhǎng),這世上最難降的妖魔是什么酣溃? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任瘦穆,我火速辦了婚禮,結(jié)果婚禮上赊豌,老公的妹妹穿的比我還像新娘扛或。我一直安慰自己,他們只是感情好碘饼,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布告喊。 她就那樣靜靜地躺著,像睡著了一般派昧。 火紅的嫁衣襯著肌膚如雪黔姜。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,328評(píng)論 1 310
  • 那天蒂萎,我揣著相機(jī)與錄音秆吵,去河邊找鬼。 笑死五慈,一個(gè)胖子當(dāng)著我的面吹牛纳寂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播泻拦,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼毙芜,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了争拐?” 一聲冷哼從身側(cè)響起腋粥,我...
    開(kāi)封第一講書(shū)人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后隘冲,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體闹瞧,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年展辞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了奥邮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡罗珍,死狀恐怖洽腺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情覆旱,我是刑警寧澤已脓,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站通殃,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏厕宗。R本人自食惡果不足惜画舌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望已慢。 院中可真熱鬧曲聂,春花似錦、人聲如沸佑惠。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)膜楷。三九已至旭咽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赌厅,已是汗流浹背穷绵。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留特愿,地道東北人仲墨。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像揍障,于是被迫代替她去往敵國(guó)和親目养。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

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

  • 前言:最近應(yīng)該有很多小伙伴去跳槽面試的吧毒嫡,相信各位有的已經(jīng)順利收到offer了癌蚁,而有些則是碰壁了,那么我在這里給大...
    iOS開(kāi)發(fā)面試總結(jié)閱讀 1,049評(píng)論 0 0
  • MJRefresh是李明杰老師的作品,到現(xiàn)在已經(jīng)有9800多顆star了匈勋,是一個(gè)簡(jiǎn)單實(shí)用礼旅,功能強(qiáng)大的iOS下拉刷新...
    Style_mao閱讀 661評(píng)論 1 2
  • MJRefresh 已經(jīng)很久沒(méi)有寫(xiě)技術(shù)文章了,之前一段時(shí)間確實(shí)也是很忙洽洁,當(dāng)然這也是一個(gè)借口痘系,自己不思進(jìn)取的成分也有...
    雨雪傳奇閱讀 1,137評(píng)論 0 4
  • 1.前言 MJRefresh 是日常 iOS 開(kāi)發(fā)中使用頻率比較高的一款下拉刷新/上拉加載更多的第三方控件,平時(shí)似...
    RiverSea閱讀 1,395評(píng)論 0 10
  • 今天感恩節(jié)哎饿自,感謝一直在我身邊的親朋好友汰翠。感恩相遇!感恩不離不棄昭雌。 中午開(kāi)了第一次的黨會(huì)复唤,身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,569評(píng)論 0 11