MJRefresh原理探究

最近要做一個(gè)下拉特效,由于平時(shí)用的是MJRefresh,所以研究一下MJRefresh源碼,下面把研究的一些心得寫出來(lái)

總體結(jié)構(gòu)

mj.png

上圖是MJRefresh在Github的圖,從上面可以看得出來(lái),首先MJRefresh是基于一個(gè)component類的,然后是基礎(chǔ)的上下拉刷新控件,然后就是基于這兩個(gè)基礎(chǔ)類的擴(kuò)展。這些基礎(chǔ)類在Base文件夾中定義了

mjbase.png

本文主要探究框架內(nèi)部實(shí)現(xiàn)原理,所以主要主要講一下基類的實(shí)現(xiàn)

MJRefreshComponent類

這是一個(gè)抽象類,平時(shí)使用都是使用它的子類去實(shí)現(xiàn),這個(gè)類主要實(shí)現(xiàn)了

  1. 初始化
  2. KVO監(jiān)聽(tīng)
  3. 定義公共方法,讓子類去實(shí)現(xiàn)

先看初始化部分

#pragma mark - 初始化
- (instancetype)initWithFrame:(CGRect)frame
{
   if (self = [super initWithFrame:frame]) {
       // 準(zhǔn)備工作
       [self prepare];
       
       // 默認(rèn)是普通狀態(tài)
       self.state = MJRefreshStateIdle;
   }
   return self;
}

- (void)prepare
{
   // 基本屬性
   self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
   self.backgroundColor = [UIColor clearColor];
}

- (void)layoutSubviews
{
   [self placeSubviews];
   
   [super layoutSubviews];
}

- (void)placeSubviews{}

- (void)willMoveToSuperview:(UIView *)newSuperview
{
   [super willMoveToSuperview:newSuperview];
   
   // 如果不是UIScrollView,不做任何事情
   if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
   
   // 舊的父控件移除監(jiān)聽(tīng)
   [self removeObservers];
   
   if (newSuperview) { // 新的父控件
       // 設(shè)置寬度
       self.mj_w = newSuperview.mj_w;
       // 設(shè)置位置
       self.mj_x = 0;
       
       // 記錄UIScrollView
       _scrollView = (UIScrollView *)newSuperview;
       // 設(shè)置永遠(yuǎn)支持垂直彈簧效果
       _scrollView.alwaysBounceVertical = YES;
       // 記錄UIScrollView最開(kāi)始的contentInset
       _scrollViewOriginalInset = _scrollView.contentInset;
       
       // 添加監(jiān)聽(tīng)
       [self addObservers];
   }
}

- (void)drawRect:(CGRect)rect
{
   [super drawRect:rect];
   
   if (self.state == MJRefreshStateWillRefresh) {
       // 預(yù)防view還沒(méi)顯示出來(lái)就調(diào)用了beginRefreshing
       self.state = MJRefreshStateRefreshing;
   }
}

從上面代碼段我們可以看到

  • init方法調(diào)用了prepare方法,prepare方法在.h文件中暴露出來(lái)給子類實(shí)現(xiàn)的,主要用來(lái)初始化一些屬性(key、高度等)
  • placeSubviews方法主要用來(lái)對(duì)子視圖進(jìn)行布局調(diào)整,也是給子類實(shí)現(xiàn)的,
  • willMoveToSuperview的注釋很詳細(xì),主要用來(lái)判斷是不是UIScrollView類或其子類,是的話就添加監(jiān)聽(tīng),所以只要是繼承UISCrollView的來(lái)都可以實(shí)現(xiàn)監(jiān)聽(tīng)
  • drawRect方法是做一些預(yù)防處理

接下來(lái)是實(shí)現(xiàn)的核心,KVO監(jiān)聽(tīng)過(guò)程

#pragma mark - KVO監(jiān)聽(tīng)
- (void)addObservers
{
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];
    self.pan = self.scrollView.panGestureRecognizer;
    [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];
}

- (void)removeObservers
{
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentOffset];
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentSize];;
    [self.pan removeObserver:self forKeyPath:MJRefreshKeyPathPanState];
    self.pan = nil;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // 遇到這些情況就直接返回
    if (!self.userInteractionEnabled) return;
    
    // 這個(gè)就算看不見(jiàn)也需要處理
    if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
        [self scrollViewContentSizeDidChange:change];
    }
    
    // 看不見(jiàn)
    if (self.hidden) return;
    if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
        [self scrollViewContentOffsetDidChange:change];
    } else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
        [self scrollViewPanStateDidChange:change];
    }
}

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}

從代碼段可以看到,對(duì)于scrollView的contentOffsetcontentSize和手勢(shì)進(jìn)行了監(jiān)聽(tīng)。當(dāng)監(jiān)聽(tīng)的內(nèi)容發(fā)生變化,就調(diào)用相應(yīng)的方法;這些方法都是空的,由子類實(shí)現(xiàn),上下拉根據(jù)不同需要實(shí)現(xiàn)不同內(nèi)容。

這部分是刷新實(shí)現(xiàn)的核心,其本質(zhì)就是通過(guò)KVO監(jiān)聽(tīng)scrollView的相關(guān)屬性來(lái)進(jìn)行不同調(diào)用實(shí)現(xiàn)的。

MJRefreshHeader類

#pragma mark - 覆蓋父類的方法
- (void)prepare
{
    [super prepare];
    
    // 設(shè)置key
    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
    
    // 設(shè)置高度
    self.mj_h = MJRefreshHeaderHeight;
}

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

從代碼可以看出,初始化主要是要設(shè)置key、高度和y坐標(biāo)值,以(0,-高度)加入到scrollView中

- (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)到下一個(gè)控制器時(shí),contentInset可能會(huì)變
     _scrollViewOriginalInset = self.scrollView.contentInset;
    
    // 當(dāng)前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 頭部控件剛好出現(xiàn)的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 如果是向上滾動(dòng)到看不見(jiàn)頭部控件,直接返回
    // >= -> >
    if (offsetY > happenOffsetY) return;
    
    // 普通 和 即將刷新 的臨界點(diǎn)
    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īng)顟B(tài)
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
            // 轉(zhuǎn)為普通狀態(tài)
            self.state = MJRefreshStateIdle;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開(kāi)
        // 開(kāi)始刷新
        [self beginRefreshing];
    } else if (pullingPercent < 1) {
        self.pullingPercent = pullingPercent;
    }
}

這段代碼有點(diǎn)多,首先if代碼段是為了解決在正在進(jìn)行刷新的時(shí)候 tableView 中 sectionHeader 停留問(wèn)題的;然后接下來(lái)的代碼主要就是對(duì)當(dāng)前contentOffsetoffsetY和頭部剛好出現(xiàn)的offsetYhappenOffsetY的計(jì)算出刷新時(shí)所處對(duì)應(yīng)的狀態(tài),設(shè)置下拉百分比。

接下來(lái)看看狀態(tài)的set方法

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStateIdle) {
        if (oldState != MJRefreshStateRefreshing) return;
        
        // 保存刷新時(shí)間
        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
        // 恢復(fù)inset和offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            self.scrollView.mj_insetT += self.insetTDelta;
            
            // 自動(dòng)調(diào)整透明度
            if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
        } completion:^(BOOL finished) {
            self.pullingPercent = 0.0;
            
            if (self.endRefreshingCompletionBlock) {
                self.endRefreshingCompletionBlock();
            }
        }];
    } else if (state == MJRefreshStateRefreshing) {
         dispatch_async(dispatch_get_main_queue(), ^{
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
                // 增加滾動(dòng)區(qū)域top
                self.scrollView.mj_insetT = top;
                // 設(shè)置滾動(dòng)位置
                [self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
            } completion:^(BOOL finished) {
                [self executeRefreshingCallback];
            }];
         });
    }
}

從代碼可以看到,這個(gè)函數(shù)主要用于在更換狀態(tài)的時(shí)候顯示每個(gè)狀態(tài)所對(duì)應(yīng)的界面,MJRefresh是通過(guò)scrollView的contentInset來(lái)顯示數(shù)顯用的header,就是在刷險(xiǎn)狀態(tài)的時(shí)候可以讓header停留在頂部,刷新完成后設(shè)置回原來(lái)的contentInset

一開(kāi)始就有這么一個(gè)宏MJRefreshCheckState,其實(shí)現(xiàn)如下

// 狀態(tài)檢查
#define MJRefreshCheckState \
MJRefreshState oldState = self.state; \
if (state == oldState) return; \
[super setState:state];

這個(gè)宏主要用來(lái)做狀態(tài)檢查的,在相同狀態(tài)下,根據(jù)上一次狀態(tài)來(lái)作不同的處理。例如剛開(kāi)始往下拉處于空閑狀態(tài)要顯示一個(gè)arrow,但是下拉完成后回到空閑狀態(tài)要把a(bǔ)rrow隱藏。

Footer實(shí)現(xiàn)

其實(shí)Footer的實(shí)現(xiàn)類似,不過(guò)具體上拉刷新有多種樣式所以需要在MJRefreshAutoFooter MJRefreshBackFooter.h做不同的處理,這里就不多說(shuō)了

小結(jié)

整個(gè)框架在基類實(shí)現(xiàn)了最基本的流程,把握整個(gè)框架把這部分弄懂了就基本可以了,每個(gè)子類只是做了不同的邏輯處理而已。

總的來(lái)說(shuō),上下拉刷新的原理就是先把刷新控件添加到scrollView的頭部或者底部,然后通過(guò)KVO監(jiān)聽(tīng)到scrollView的滾動(dòng)進(jìn)度(底部刷新還需要監(jiān)控scrollView的內(nèi)容的改變,每次改變后再次將控件調(diào)整到scrollView的底部),根據(jù)不同的進(jìn)度來(lái)設(shè)置控件的相應(yīng)的文字和圖片動(dòng)畫等。

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,026評(píng)論 4 61
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,725評(píng)論 25 709
  • 今天報(bào)了30天跑團(tuán),可是下班回家路上冷颼颼的風(fēng)就讓我打起了退堂鼓。 回家呼嚕嚕吃了一堆零食,然后陷入深深的糾結(jié)中 ...
    貓小埋閱讀 264評(píng)論 0 0
  • 01 凜冬已至,王昭君出現(xiàn)在了峽谷。 李白正在對(duì)面的野區(qū)刷怪,瞧著敵方陣營(yíng)的韓信身影閃了一下,卻沒(méi)有來(lái)找他,反而去...
    納蘭榴蓮閱讀 18,813評(píng)論 9 42
  • 雖然不過(guò)半個(gè)多小時(shí)的車程,但由于親人們的逐漸疏離,自己往往是難得回一次老家。前幾天,老家的表叔打電話來(lái)邀請(qǐng)我們一家...
    蔚藍(lán)楓葉1970閱讀 494評(píng)論 2 1

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