MJRefresh源碼解析

首先看下結構圖
這裏寫圖片描述

MJRefreshComponent基類

基類裏面爲我們捕獲了當前的父view(UITableView、UICollectionVeiw等),然後使用KVO爲我們做了一些滑動操作屬性的監聽,具體的處理需要交給子類去實現。

初始化

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        // 準備工作
        [self prepare];

        // 默認是普通狀態
        self.state = MJRefreshStateIdle;
    }
    return self;
}

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

- (void)layoutSubviews
{
    [super layoutSubviews];

    [self placeSubviews];
}
  • (void)placeSubviews{}
    這裏設計了兩個方法,分別交給子類去使用。

  • 一個是preppare方法,觸發是通過initWithFrame,這個方法是作用就是初始化一些基本的屬性、狀態等,傾向於數據方面的初始化。

  • 一個是placeSubviews方法,觸發是通過layoutSubviews。關於layoutSubviews的觸發條件不清楚的同學可以Google一下。這個方法傾向於處理UI方面。
    屬性方法的轉換處理
- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];

    // 如果不是UIScrollView,不做任何事情
    if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;

    // 舊的父控件移除監聽
    [self removeObservers];

    if (newSuperview) { // 新的父控件
        // 設置寬度
        self.mj_w = newSuperview.mj_w;
        // 設置位置
        self.mj_x = 0;

        // 記錄UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // 設置永遠支持垂直彈簧效果
        _scrollView.alwaysBounceVertical = YES;
        // 記錄UIScrollView最開始的contentInset
        _scrollViewOriginalInset = _scrollView.contentInset;

        // 添加監聽
        [self addObservers];
    }
}

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];

    if (self.state == MJRefreshStateWillRefresh) {
        // 預防view還沒顯示出來就調用了beginRefreshing
        self.state = MJRefreshStateRefreshing;
    }
}

pragma mark - KVO監聽

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

    // 這個就算看不見也需要處理
    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];
    }
}
  • (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
  • (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
  • (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
    此處關鍵的切入點是繼承父類的willMoveToSuperview方法,這個方法在刷新控件將要加載到父類的時候調用,這樣我們可以獲取到父類。然後如果是新的父類的話,首先初始化一些變量屬性,然後添加KVO,KVO在這裏的作用是監聽@”contentOffset”,@”contentSize”以及@”state”屬性的變化,來對header,footer做響應狀態的處理。

此處爲子類拋出了三個響應的方法(scrollViewContentOffsetDidChange…)可以繼承下來,實現不同的功能。

刷新狀態的處理

pragma mark 進入刷新狀態

- (void)beginRefreshing
{
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
        self.alpha = 1.0;
    }];
    self.pullingPercent = 1.0;
    // 只要正在刷新,就完全顯示
    if (self.window) {
        self.state = MJRefreshStateRefreshing;
    } else {
        self.state = MJRefreshStateWillRefresh;
        // 刷新(預防從另一個控制器回到這個控制器的情況,回來要重新刷新一下)
        [self setNeedsDisplay];
    }
}

pragma mark 結束刷

- (void)endRefreshing
{
    self.state = MJRefreshStateIdle;
}agma mark 是否正在刷新
- (BOOL)isRefreshing
{
    return self.state == MJRefreshStateRefreshing || self.state == MJRefreshStateWillRefresh;
}

開始結束刷新狀態和判斷是不是正在刷新狀態,其中除了動畫外,self.state是個很重要的屬性,幾乎所有的狀態變化都是state的set方法裏面實現,下面我們會在子類中解讀。

回調方法

- (void)executeRefreshingCallback
{
    dispatch_async(dispatch_get_main_queue(), ^{
        if (self.refreshingBlock) {
            self.refreshingBlock();
        }
        if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
            MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
        }
    });
}

對於回調方法,這裏支持block和target:selecteor:兩種。此處target方式的調用是用的封裝的runtime宏,我們知道方法的調用實際上就是轉換成objc_msgSend方法。

這種封裝對於我們也是有借鑑意義的,如果寫公共的框架,我們也是應該支持多種類型的回調的。最好再加一個delegate的方法。

Header控件

下面一步一步解析繼承的子類的功能以及職責。

MJRefreshHeader

繼承自MJRefreshComponent基礎父類。

本類的主要職責:
實現基本的頁面刷新邏輯,負責解析出各個狀態,然後對每個狀態進行處理。(簡單點說就是隻是邏輯的實現,簡單下拉,然後scrollView頭部留出一點空,等待刷新完成,完成後空白消失)。

這裏對contentoffset的狀態的變化進行監聽,並對結果處理成四種狀態:

MJRefreshStateIdle = 1, /* 普通閒置狀態 /
MJRefreshStatePulling, /* 鬆開就可以進行刷新的狀態 /
MJRefreshStateRefreshing, /* 正在刷新中的狀態 /
MJRefreshStateWillRefresh, /* 即將刷新的狀態 /

mj
先解讀一下初始化方法:

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock {
    MJRefreshHeader *cmp = [[self alloc] init];
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}
這裏封裝了一個類方法,方便用戶調用,並獲取刷新的回調。

- (void)prepare {
    [super prepare];
    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
    self.mj_h = MJRefreshHeaderHeight;
}

- (void)placeSubViews {
    [super placeSubViews];

    // 設置y值(當自己的高度發生改變了,肯定要重新調整Y值,所以放到placeSubviews方法中設置y值)
    self.mj_y =  - self.mj_h - self.ignoredScrollViewContentInsetTop;
}

然後prepare就是跟之前說的一樣,設置一下初始化值數據。而placeSubViews這裏更新一下UI。

通過contentofffset轉換刷新的狀態

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

    // 在刷新的refreshing狀態
    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;
    }

    // 跳轉到下一個控制器時,contentInset可能會變
     _scrollViewOriginalInset = self.scrollView.contentInset;

    // 當前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 頭部控件剛好出現的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) {
            // 轉爲即將刷新狀態
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
            // 轉爲普通狀態
            self.state = MJRefreshStateIdle;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手鬆開
        // 開始刷新
        [self beginRefreshing];
    } else if (pullingPercent < 1) {
        self.pullingPercent = pullingPercent;
    }
}
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState

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

        // 保存刷新時間
        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
        [[NSUserDefaults standardUserDefaults] synchronize];

        // 恢復inset和offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            self.scrollView.mj_insetT += self.insetTDelta;

            // 自動調整透明度
            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;
                // 增加滾動區域top
                self.scrollView.mj_insetT = top;
                // 設置滾動位置
                [self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
            } completion:^(BOOL finished) {
                [self executeRefreshingCallback];
            }];
         });
    }
}

下面我們一起解讀一下這個刷新的過程。

首先,我們下拉刷新的時候,當前狀態肯定是MJRefreshStateIdle。那麼直接走下面的if判斷,滿足第一個if,當前操作是isDragging。這裏計算出了拖拽所佔的比例,也就下拉的百分比。

如果是MJRefreshStateIdle普通狀態並超出下拉超出header的高度,那麼狀態立刻轉換爲MJRefreshStatePulling拖動中。

此時如果沒有繼續下拉,而是鬆手,那麼就會走else if,立刻把狀態轉換回MJRefreshStateIdle。

此時如果繼續下拉,那麼走下面這個else if (self.state == MJRefreshStatePulling),立刻調用beginRefreshing開始刷新。其實也就是把狀態轉換爲MJRefreshStateRefreshing.看一下父類中的實現:

- (void)beginRefreshing {
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
        self.alpha = 1.0;
    }];
    self.pullingPercent = 1.0;
    if (self.window) {
        self.state = MJRefreshStateRefreshing;
    }
    else {
        if (self.state != MJRefreshStateRefreshing) {
            self.state = MJRefreshStateWillRefresh;
            [self setNeedsDisplay]; // 繼續調用drawrect,這樣延時,加了個willrefresh的state 意欲:預防從另一個控制器回到這個控制器的情況,回來要重新刷新一下
        }
    }
}

這裏做了一個有時間長度的動畫,多加了一個willRefresh的狀態,我的理解是爲了防止從另一個頁面返回的時候self.window爲空的時候,突然刷新會崩潰,需要一個狀態來過渡。當然了當前的下拉比例是百分百了。

設置state = MJRefreshStateRefreshing,會調用state的setStaus方法,我們看到,這裏直接對頁面進行了header高度的偏移,使得整個header顯示出來,然後在完成動畫的時候回調回去,我們可以進行一些網絡請求。

此時狀態是MJRefreshStateRefreshing,繼續監聽會接着走第一個if:if (self.state == MJRefreshStateRefreshing),這裏重新設置了insetTop,也就是頁面偏移到整個header的高度,好像跟setState有點重複。記錄了一下insetTDelta值::初始的top - insetTop, 這個值是刷新完恢復header位置用的。當然了,這裏也是爲了兼容,我們使用MJ之前設置過了insetTop,這裏是在之前設置過的insetTop基礎上再偏移的。然後就是return,下面的代碼不會再執行。

直到我們的網絡請求完成,觸發endRefreshing方法,那麼此時的狀態會改變回去MJRefreshStateIdle。然後處理就是保存一下刷新的時間,通過這個insetTDelta恢復insetTop爲原始值,完成後執行endRefreshingCompletionBlock回調。

MJRefreshStateHeader

此部分繼承自MJRefreshHeader,主要就是處理狀態文字,不同狀態下的提示文字是不同的。系統給我們提供了多語言版本的默認提示語,當然我們也可以自己設置我們的個性提示語:

- (void)setTitle:(NSString *)title forState:(MJRefreshState)state {
    if (title == nil) {
        return;
    }
    self.stateTitles[@(state)] = title;
    self.stateLabel.text = self.stateTitles[@(self.state)];
}

這裏是用的state枚舉值當做key來存的stateTitle值,使用的是字典。

還是使用這兩個初始化方法:

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

- (void)placeSubviews
{
    [super placeSubviews];

    if (self.stateLabel.hidden) return;

    BOOL noConstrainsOnStatusLabel = self.stateLabel.constraints.count == 0;

    if (self.lastUpdatedTimeLabel.hidden) {
        // 狀態
        if (noConstrainsOnStatusLabel) self.stateLabel.frame = self.bounds;
    } else {
        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;
        }

        // 更新時間
        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;
        }
    }
}

prepare方法默認設置了狀態文字,placeSubviews方法對文字Label進行處理。這裏一共兩個label,一個狀態文字label,一個上次刷新時間label。並對隱藏一個或者全部做了頁面佈局的處理。

最後還是通過state的set方法來更新狀態:

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

    // 設置狀態文字
    self.stateLabel.text = self.stateTitles[@(state)];

    // 重新設置key(重新顯示時間)
    self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}

不得不說setState方法是核心處理方法,很重要。

MJRefreshNormalHeader

此部分繼承自MJRefreshStateHeader,作用就是在添加了文字基礎上,添加了動畫部分,包括刷新的菊花和箭頭。

箭頭的添加方式跟文本添加是一樣的,添加在了文本的左邊,如果文本隱藏的話,箭頭就居中了。

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

    // 根據狀態做事情
    if (state == MJRefreshStateIdle) {
        if (oldState == MJRefreshStateRefreshing) {
            self.arrowView.transform = CGAffineTransformIdentity;

            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                self.loadingView.alpha = 0.0;
            } completion:^(BOOL finished) {
                // 如果執行完動畫發現不是idle狀態,就直接返回,進入其他狀態
                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的動畫完畢動作沒有被執行
        [self.loadingView startAnimating];
        self.arrowView.hidden = YES;
    }
}

在state的set方法中,添加了一個箭頭的旋轉動畫。

MJRefreshGifHeader

與MJRefreshNormalHeader類似,繼承自MJRefreshStateHeader。是在文本類的基礎上添加的帶有普通動畫的子類。

只不過這裏加了拖動和刷新中的動畫。

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

    // 根據狀態做事情
    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];
    }
}

這裏使用的是UIImageView的自帶圖片動畫功能。我們可以直接傳入圖片數組來實現動畫。當然如果不能滿足我們的需求,我們是可以自己封裝這一層,替換爲自己的動畫。

Footer控件

Footer封裝了兩個分支子類:

  • BackFooter(會回彈到底部的上拉刷新控件)
  • AutoFooter(會自動刷新的上拉刷新控件)
    MJRefreshFooter

基礎父類,是兩種footer的共同父類。此處比Header的父類要簡單一些,因爲下面兩個分支的邏輯處理還是不一樣的,所以這裏就簡單的做了一些初始化工作。

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

    if (newSuperview) {
        // 監聽scrollView數據的變化
        if ([self.scrollView isKindOfClass:[UITableView class]] || [self.scrollView isKindOfClass:[UICollectionView class]]) {
            [self.scrollView setMj_reloadDataBlock:^(NSInteger totalDataCount) {
                if (self.isAutomaticallyHidden) {
                    self.hidden = (totalDataCount == 0);
                }
            }];
        }
    }
}

這裏可以設置自動隱藏Footer。前提是tableview或collectionview的數據源爲空。

AutoFooter

自動上拉加載類。滑動到底部觸發刷新動作,無需上拉操作。

MJRefreshAutoFooter

類似於MJRefreshHeader類的功能,將UI效果邏輯處理出來。

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

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

    if (_scrollView.mj_insetT + _scrollView.mj_contentH > _scrollView.mj_h) { // 內容超過一個屏幕
        // 這裏的_scrollView.mj_contentH替換掉self.mj_y更爲合理
        if (_scrollView.mj_offsetY >= _scrollView.mj_contentH - _scrollView.mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + _scrollView.mj_insetB - self.mj_h) { 
                /*
            // contentsize Height + bottom = scrollview content 全部展示高度。    注意此處的self.hegiht 包含在bottom裏面
            // _scrollView.mj_contentH +  _scrollView.mj_insetB - _scrollView.mj_h - self.mj_h * (1 - self.triggerAutomaticallyRefreshPercent)  
             // if  self.triggerAutomaticallyRefreshPercent = 1, 跟下面的 第二中情況是一樣的。 只是兼容了一下 可以顯示self的多少比例,就刷新而已。 其實此處默認也是1.  但是這個方法比下面的方法,提前回調。呵呵。
            // 這個方法跟下面的方法區別在於,下面的方法需要是手鬆開的情況下,達到臨界值,而這個方法是不需要的,也就是條件更寬鬆。其實,如果手勢滑動到底部情況下,這個方法,和下面的方法的處理是一樣的。效果也是一樣的。
            */
            // 防止手鬆開時連續調用
            CGPoint old = [change[@"old"] CGPointValue];
            CGPoint new = [change[@"new"] CGPointValue];
            if (new.y <= old.y) return;

            // 當底部刷新控件完全出現時,才刷新
            [self beginRefreshing];
        }
    }
}

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

    if (self.state != MJRefreshStateIdle) return;

    if (_scrollView.panGestureRecognizer.state == UIGestureRecognizerStateEnded) {// 手鬆開
        if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h) {  // 不夠一個屏幕
            if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { // 向上拽, 拽的浮度需要大於top的值 沒有設置的話此處是mj_insetT = 0,注意此處的mj_offsetY已經默認是加了初始top的offset。 比如設置top 100 ,此處offset = -100 ,只要向上滑動一點,就可以執行下面的刷新。
                [self beginRefreshing];
            }
        } else { // 超出一個屏幕, // 這個是需要拖一下。再加載, 正常情況下,下面這個開始加載是不會走的,只有設置automaticallyRefresh = NO 纔會走這個。
            if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView.mj_insetB - _scrollView.mj_h) { // 也就是比偏移量比contentH + Bottom 整個scrollview的顯示內容 - scrollview的height還有大。 相等是到底部,大於就是有個拖動手勢。
                [self beginRefreshing];
            }
        }
    }
}

這裏處理了contentOffset的變化以及Pan手勢的改變狀態回調。

MJRefreshAutoStateFooter

封裝思路類似於MJRefreshStateHeader,不再贅述。

MJRefreshAutoNormalFooter

封裝思路類似於MJRefreshNormalHeader,不再贅述。

MJRefreshAutoGifFooter

封裝思路類似於MJRefreshGifHeader,不再贅述。

BackFooter

跟Header的封裝思路是一樣的,需要上拉纔會觸發刷新動作。

MJRefreshBackFooter

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

    // 如果正在刷新,直接返回
    if (self.state == MJRefreshStateRefreshing) return;

    _scrollViewOriginalInset = self.scrollView.contentInset;

    // 當前的contentOffset
    CGFloat currentOffsetY = self.scrollView.mj_offsetY;
    // 尾部控件剛好出現的offsetY
    CGFloat happenOffsetY = [self happenOffsetY];
    // 如果是向下滾動到看不見尾部控件,直接返回
    if (currentOffsetY <= happenOffsetY) return;

    CGFloat pullingPercent = (currentOffsetY - happenOffsetY) / self.mj_h;

    // 如果已全部加載,僅設置pullingPercent,然後返回
    if (self.state == MJRefreshStateNoMoreData) {
        self.pullingPercent = pullingPercent;
        return;
    }

    if (self.scrollView.isDragging) {
        self.pullingPercent = pullingPercent;
        // 普通 和 即將刷新 的臨界點
        CGFloat normal2pullingOffsetY = happenOffsetY + self.mj_h;

        if (self.state == MJRefreshStateIdle && currentOffsetY > normal2pullingOffsetY) {
            // 轉爲即將刷新狀態
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && currentOffsetY <= normal2pullingOffsetY) {
            // 轉爲普通狀態
            self.state = MJRefreshStateIdle;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手鬆開
        // 開始刷新
        [self beginRefreshing];
    } else if (pullingPercent < 1) {
        self.pullingPercent = pullingPercent;
    }
}

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

    // 內容的高度
    CGFloat contentHeight = self.scrollView.mj_contentH + self.ignoredScrollViewContentInsetBottom;
    // 表格的高度
    CGFloat scrollHeight = self.scrollView.mj_h - self.scrollViewOriginalInset.top - self.scrollViewOriginalInset.bottom + self.ignoredScrollViewContentInsetBottom;
    // 設置位置和尺寸
    self.mj_y = MAX(contentHeight, scrollHeight);
}

對比之後,我們發現跟Header的思路基本一致。

MJRefreshBackStateFooter

封裝思路類似於MJRefreshStateHeader,不再贅述。

MJRefreshBackNormalFooter

封裝思路類似於MJRefreshNormalHeader,不再贅述。

MJRefreshBackGifFooter

封裝思路類似於MJRefreshGifHeader,不再贅述。

轉載請註明出處:http://semyonxu.com

發佈了26 篇原創文章 · 獲贊 7 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章