使用Xtrace分析MJRefresh技術(shù)實(shí)現(xiàn)細(xì)節(jié)(二):動(dòng)態(tài)變化

寫(xiě)在前面

上一篇,我們利用Xtrace詳細(xì)地分析了MJRefresh在UIView生命周期的基礎(chǔ)上,做了哪些自定義修改。
本篇,將繼續(xù)分析其最重要的部分,動(dòng)態(tài)變化。

一、下拉刷新的實(shí)現(xiàn)原理

這部分,本想在第一篇介紹,但發(fā)現(xiàn)實(shí)現(xiàn)原理跟動(dòng)態(tài)實(shí)現(xiàn)這篇聯(lián)系比較緊密,所以還是放在這里寫(xiě)吧。

(一)初始狀態(tài)

TableView基本布局
運(yùn)行圖

通過(guò)上兩張圖,想必大家看出來(lái)了,MJRefresh的初始狀態(tài)下的布局,就是很簡(jiǎn)單的在UITableView可視View的上部附加了一個(gè)視圖。這樣當(dāng)我們下拉的時(shí)候,這個(gè)部分的視圖就會(huì)顯示出來(lái)。

(二)“松手刷新”狀態(tài)

松手刷新

這部分的實(shí)現(xiàn)原理也很簡(jiǎn)單,通過(guò)監(jiān)聽(tīng)TableView的origin,當(dāng)其超過(guò)一定數(shù)值的時(shí)候,就對(duì)視圖中的組件(這里是Arrow.png 和 Label)做動(dòng)畫(huà)。

當(dāng)然光判斷orgin還是不行的,像上圖那種情況,用戶可能會(huì)放棄刷新,所以還需要判斷Pan手勢(shì)的狀態(tài),這部分下文我們?cè)僭斦劇?/p>

(三)刷新?tīng)顟B(tài)

刷新?tīng)顟B(tài)

這個(gè)狀態(tài)涉及到的主要部分是TableView.contentInset屬性,通過(guò)修改ContentInset變相修改origin,實(shí)現(xiàn)Subviews的整體下移。
刷新結(jié)束之后,再將ContentInset復(fù)位。
對(duì)這部分不熟悉的童鞋,可以參考我之前的文章。

二、MJRefresh的實(shí)現(xiàn)方式

科學(xué)是共享的,技術(shù)可不是共享的。

好比汽車(chē)發(fā)動(dòng)機(jī),大家都知道原理很簡(jiǎn)單,但是為什么中國(guó)制造到現(xiàn)在還是比不過(guò)國(guó)外?很簡(jiǎn)單,科學(xué)原理就是那樣,但是技術(shù),那是西方列強(qiáng)一百多年的經(jīng)驗(yàn)沉淀,而且對(duì)中國(guó)實(shí)施技術(shù)封鎖,比不上人家也是必然的。

一不小心跑題了…………
下拉刷新的原理不難,而且也有很多第三方庫(kù)的封裝,MJRefresh目前應(yīng)該還是Star最多的組件,好在這是開(kāi)源的,可以讓我們一睹芳容。

(一)初始化

MJRefresh在初始化這部分的代碼的時(shí)候,只用到了KVO,監(jiān)聽(tīng)了ContentSIze、ContentInset和PanGesture三個(gè)屬性,具體代碼如下:

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

子類(lèi)通過(guò)覆蓋DidChange方法,實(shí)現(xiàn)自定義部分。

(二)運(yùn)行時(shí)總體流程

函數(shù)調(diào)用

很平常的一個(gè)KVO過(guò)程:

  1. 通知MJRefresh,你監(jiān)聽(tīng)的屬性變化了
  2. MJRefresh接過(guò)通知
  3. 處理通知

(三)實(shí)現(xiàn)細(xì)節(jié)

這部分就是MJRefresh的核心部分了,其中有的部分,我也沒(méi)有弄得太明白,畢竟MJRefresh迭代了這么多版本,中間修復(fù)了很多BUG,這些BUG場(chǎng)景我可能都沒(méi)見(jiàn)過(guò),所以不涉及到核心邏輯部分的代碼,我就不細(xì)說(shuō)了,免得理解錯(cuò)了。

強(qiáng)勢(shì)插入

為了更好地理解,最好先回顧一下在你簡(jiǎn)單拖動(dòng)的時(shí)候,到底發(fā)生了什么:
其中SubView的SuperView為T(mén)ableView

subView.actualY = subView.frame.y - tableView.origin.y   //公式一

tableView.origin.y = tableView.original.origin.y(初始值) -  panGesture.location.y(touch坐標(biāo))  // 公式二

公式二代入公式一中,可以得出:

 subView.actualY = tableView.subView.frame.y - tableView.original.origin.y + panGesture.location.y

其中subView.frame 與 tableView.original.origin.y 皆為常數(shù),也就是說(shuō)

subView.actualY = panGesture.location.y + const

即Subviews的實(shí)際位置,是與手指位置正相關(guān)的:
當(dāng)你手指向下運(yùn)動(dòng),即下拉時(shí),gesture.location.y 在增大,subViews的實(shí)際位置就會(huì)下移;
當(dāng)你手指向上運(yùn)動(dòng),即上滑時(shí),gesture.location.y 在減小,subViews的實(shí)際位置就會(huì)上移。

3.0 MJRefresh的核心處理代碼,在MJRefreshHeader文件中,其中主要是兩個(gè)方法:

  • scrollViewContentOffsetDidChange
  • setState

MJ本人正在辦教育,所以代碼部分自己也加了不少注釋,我只是在他的基礎(chǔ)上,增加了一些方便理解的注釋。

先介紹setState,是因?yàn)閛ffsetDidChange方法中會(huì)直接對(duì)state屬性進(jìn)行設(shè)置,也就是說(shuō),offsetDidChange方法的實(shí)現(xiàn)依賴于state。

3.1 setState

先上源代碼:

- (void)setState:(MJRefreshState)state
{

//第一步,判斷狀態(tài)是否有改變,沒(méi)有改變則直接返回
    MJRefreshState oldState = self.state;
    if (state == oldState) return; 

     [super setState:state];
    //第二步,根據(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];
            }];
         });
    }
}
setState流程圖

整個(gè)基類(lèi)方法中,并不對(duì)SubView進(jìn)行設(shè)置,主要是為了設(shè)置ContentInset,具體SubView的動(dòng)畫(huà)則由子類(lèi)去實(shí)現(xiàn)。

大體可以分為兩步:

1.主要確定是從什么樣的state轉(zhuǎn)換成現(xiàn)在的state的,即:what's oldstate → new state

2.根據(jù)不同的切換方式,設(shè)置特定的ContentInset的值:

  • Idle→Pulling,基類(lèi)不做處理,子類(lèi)自定義實(shí)現(xiàn)
  • 從Pulling→Refreshing,設(shè)定ContentInset,使RefreshView能夠懸停
  • Refreshing→Pulling,不存在,因?yàn)樵贓ndRefreshing方法中,直接切換狀態(tài)為Idle
  • 從Refreshing→Idle,則恢復(fù)ContentInset,使TableView回彈到正確的位置,隱藏RefreshView

3.1 scrollViewContentOffsetDidChange:

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    //調(diào)用Component的方法,Component這部分代碼什么也沒(méi)做
    [super scrollViewContentOffsetDidChange:change];

    // 下面的If代碼塊貌似是為了解決sectionHeader的懸停問(wèn)題而存在的,跟我們介紹原理關(guān)系不大,可以選擇性忽略
    if (self.state == MJRefreshStateRefreshing) {
         //如果還沒(méi)有加入View Hierarchy則直接返回
        if (self.window == nil) return;
        // sectionheader停留解決
            //insetT = fmax(-offsetY,contentInset.top)
        CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
            //insetT = fmin(insetT, self.mj_h +contentInset.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;
    }
}
offsetDidChange流程圖

其中,方法beginRefreshing的代碼如下:

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

由我們自己調(diào)用的EndRefreshing方法代碼如下:

- (void)endRefreshing
{
    self.state = MJRefreshStateIdle;
}

整個(gè)方法,忽略掉解決Section懸停問(wèn)題的部分代碼,首先進(jìn)行判斷的就是PanGesture.state,確定用戶是否還在拖曳。

  • 只有在用戶松手并且self.state==Pullingstate的情況下,才會(huì)進(jìn)入刷新?tīng)顟B(tài)。
  • 其余情況,會(huì)根據(jù)下拉的位移量與閾值大小的比較結(jié)果,在Idle和Pulling狀態(tài)之間來(lái)回切換。

有幾個(gè)參數(shù)需要解釋一下:

happenOffsetY

該參數(shù)的值是初始化狀態(tài)下的TableView.origin.y,其主要作用是判斷MJRefreshView是否需要顯示。
參考上面的公式二

tableView.origin.y = tableView.original.origin.y(初始值) - panGesture.location.y(touch坐標(biāo))

tableView.contentOffset.y = happenOffsetY - panGesture.location.y

當(dāng)contentOffsetY > happenOffsetY 時(shí),說(shuō)明用戶正在下滑或者下滑之后還沒(méi)有返回到初始狀態(tài),這時(shí)候MJRefreshView是沒(méi)有顯示的,所以直接返回就好了;
當(dāng)contentOffsetY < happenOffsety 時(shí),情況正好相反,我們就需要開(kāi)始處理數(shù)據(jù)了。

normal2pullingOffsetY
  • 顧名思義,該參數(shù)是判斷是否需要從Normal狀態(tài)轉(zhuǎn)換為Pulling狀態(tài)的閾值。也就是說(shuō),下拉的位移量(abs(nowOffset.y - originnal.offset.y))要不小于此參數(shù)的絕對(duì)值才能進(jìn)入Pulling狀態(tài)。
  • 其實(shí)際值為MJRefreshView.height + originalContentInset.top,即完全顯示MJRefreshView所需要的高度。
    比如說(shuō):
    嵌入NavigationBar的話,其值為:-54 - 64 = -118

三、總結(jié)

至此,MJRefresh的實(shí)現(xiàn)細(xì)節(jié)部分,終于分析完了。

從整體上來(lái)看,MJRefresh采用了Template模式,即模板模式。
通過(guò)基類(lèi)定義整體的流程和共用方法,由子類(lèi)去延遲實(shí)現(xiàn)特定的方法,這樣可以在不破壞整個(gè)算法結(jié)構(gòu)的同時(shí),重新定義該算法的某些步驟。
不得不說(shuō),MJRefresh在基類(lèi)中定義的方法實(shí)現(xiàn),都很?chē)?yán)謹(jǐn),確實(shí)是一個(gè)優(yōu)秀的第三方庫(kù)。
所以,一般情況下,直接使用MJRefresh作為刷新控件是一個(gè)很好的選擇,當(dāng)我們有自己的需求的時(shí)候,可以很方面的繼承MJRefreshHeader,實(shí)現(xiàn)自定義的RefreshView。

最后編輯于
?著作權(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)容

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