MJRefresh源碼分析

MJRefresh代碼框架

MJRefresh代碼框架示意圖

MJRefreshComponent——基類

主要職能

  • 聲明控件的所有狀態(tài)
  • 聲明控件的回調函數(shù)
  • 添加監(jiān)聽
  • 提供刷新,停止刷新接口
  • 透明度設置和自動切換
  • 提供子類需要實現(xiàn)的方法

代碼解析

聲明控件的所有狀態(tài)
/** 刷新控件的狀態(tài) */
typedef NS_ENUM(NSInteger, MJRefreshState) {
    /** 普通閑置狀態(tài) */
    MJRefreshStateIdle = 1,
    /** 松開就可以進行刷新的狀態(tài) */
    MJRefreshStatePulling,
    /** 正在刷新中的狀態(tài) */
    MJRefreshStateRefreshing,
    /** 即將刷新的狀態(tài) */
    MJRefreshStateWillRefresh,
    /** 所有數(shù)據(jù)加載完畢,沒有更多的數(shù)據(jù)了 */
    MJRefreshStateNoMoreData
};

聲明控件的回調函數(shù)

/** 進入刷新狀態(tài)的回調 */
typedef void (^MJRefreshComponentRefreshingBlock)(void);
/** 開始刷新后的回調(進入刷新狀態(tài)后的回調) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)(void);
/** 結束刷新后的回調 */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)(void);

/** 正在刷新的回調 */
@property (copy, nonatomic) MJRefreshComponentRefreshingBlock refreshingBlock;
/** 開始刷新后的回調(進入刷新狀態(tài)后的回調) */
@property (copy, nonatomic) MJRefreshComponentbeginRefreshingCompletionBlock beginRefreshingCompletionBlock;
/** 結束刷新的回調 */
@property (copy, nonatomic) MJRefreshComponentEndRefreshingCompletionBlock endRefreshingCompletionBlock;

添加監(jiān)聽

#pragma mark - KVO監(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];
}

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

提供刷新,停止刷新接口

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

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

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

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

#pragma mark 是否正在刷新
- (BOOL)isRefreshing
{
    return self.state == MJRefreshStateRefreshing || self.state == MJRefreshStateWillRefresh;
}
beginRefresh
  • view未加載的時候進行刷新操作,先把狀態(tài)置為WillRefresh,等調用drawRect的時候再置為refreshing。
  • 要判斷當前狀態(tài)是否為refreshing是因為刷新結束將狀態(tài)置為idle的時候,要判斷上一個狀態(tài)是不是refreshing,如果是,才回置頭部控件的inset

透明度設置和自動切換

#pragma mark 自動切換透明度
- (void)setAutoChangeAlpha:(BOOL)autoChangeAlpha
{
    self.automaticallyChangeAlpha = autoChangeAlpha;
}

- (BOOL)isAutoChangeAlpha
{
    return self.isAutomaticallyChangeAlpha;
}

- (void)setAutomaticallyChangeAlpha:(BOOL)automaticallyChangeAlpha
{
    _automaticallyChangeAlpha = automaticallyChangeAlpha;
    
    if (self.isRefreshing) return;
    
    if (automaticallyChangeAlpha) {
        self.alpha = self.pullingPercent;
    } else {
        self.alpha = 1.0;
    }
}

#pragma mark 根據(jù)拖拽進度設置透明度
- (void)setPullingPercent:(CGFloat)pullingPercent
{
    _pullingPercent = pullingPercent;
    
    if (self.isRefreshing) return;
    
    if (self.isAutomaticallyChangeAlpha) {
        self.alpha = pullingPercent;
    }
}

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

#pragma mark - 交給子類們去實現(xiàn)
/** 初始化 */
- (void)prepare NS_REQUIRES_SUPER;
/** 擺放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 當scrollView的contentOffset發(fā)生改變的時候調用 */
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當scrollView的contentSize發(fā)生改變的時候調用 */
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當scrollView的拖拽狀態(tài)發(fā)生改變的時候調用 */
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;

初始化函數(shù)調用時機和主要功能

prepare
  • 調用時機:初始化的時候調用
  • 主要功能:設置一些基本屬性
willMoveToSuperview
  • 調用時機:作為子控件添加給scrollview的時候
  • 主要功能:在此之前只是生成了一個header或footer實例,但沒有掛到任何superview上,也沒有任何行為。觸發(fā)此函數(shù)時,把view添加到scrollview的subviews里,并保留了一個scrollview的引用,后續(xù)可以從上面獲取各種屬性,根據(jù)scrollview設置mj_w、mj_x等屬性,同時監(jiān)聽scrollview,此時可以根據(jù)scrollview的變化進行狀態(tài)的改變。
placeSubviews
  • 調用時機:觸發(fā)layoutSubviews時(addSubview、Frame修改、子控件大小修改)
  • 主要功能:修改和size相關的屬性,比如設置mj_y,子控件的屬性
drawRect
  • 調用時機:view加載的時候
  • 主要功能:view加載前要進入調用了beginRefreshing方法,此時無法進行刷新操作,所以將state設置為MJRefreshStateWillRefresh。當view加載出來后,在drawRect內將state設置為MJRefreshStateRefreshing,進行刷新操作。
#pragma mark - 初始化
- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        // 準備工作
        [self prepare];
        
        // 默認是普通狀態(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)聽
    [self removeObservers];
    
    if (newSuperview) { // 新的父控件
        // 設置寬度
        self.mj_w = newSuperview.mj_w;
        // 設置位置
        self.mj_x = -_scrollView.mj_insetL;
        
        // 記錄UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // 設置永遠支持垂直彈簧效果
        _scrollView.alwaysBounceVertical = YES;
        // 記錄UIScrollView最開始的contentInset
        _scrollViewOriginalInset = _scrollView.mj_inset;
        
        // 添加監(jiān)聽
        [self addObservers];
    }
}

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
    
    if (self.state == MJRefreshStateWillRefresh) {
        // 預防view還沒顯示出來就調用了beginRefreshing
        self.state = MJRefreshStateRefreshing;
    }
}

MJRefreshHeader

主要職能

  • 提供構造方法
  • 通過覆蓋父類的方法實現(xiàn)下拉刷新功能

主要模塊

構造方法
#pragma mark - 構造方法
+ (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;
}
覆蓋父類初始化方法
#pragma mark - 覆蓋父類的方法
- (void)prepare
{
    [super prepare];
    
    // 設置key
    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
    
    // 設置高度
    self.mj_h = MJRefreshHeaderHeight;
}

- (void)placeSubviews
{
    [super placeSubviews];
    
    // 設置y值(當自己的高度發(fā)生改變了,肯定要重新調整Y值,所以放到placeSubviews方法中設置y值)
    self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}
監(jiān)聽contentOffset,改變當前刷新狀態(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;
    }
    
    // 跳轉到下一個控制器時,contentInset可能會變
     _scrollViewOriginalInset = self.scrollView.mj_inset;
    
    // 當前的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) {
            // 轉為即將刷新狀態(tài)
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
            // 轉為普通狀態(tài)
            self.state = MJRefreshStateIdle;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
        // 開始刷新
        [self beginRefreshing];
    } else if (pullingPercent < 1) {
        self.pullingPercent = pullingPercent;
    }
}
當控間處于MJRefreshStateRefreshing狀態(tài)時
  • 如果有contentOffset的改變,保證此時的inset位于初始inset和初始inset加控件高度之間。并計算inset的變化值,用于刷新結束重置inset
當控間不處于MJRefreshStateRefreshing狀態(tài)時(idle或pulling狀態(tài))
  • 比較當前offset和happenOffsetY(頭部控件剛好出現(xiàn)的offset),如果是向上滾動到看不見頭部控件,直接返回。
  • 根據(jù)happenOffsetY和頭部控件的高度mj_h計算MJRefreshStatePulling的臨界點,以及當前從idle到Pulling的百分比pullingPercent。
  • 如果當前scrollview處于drag狀態(tài),1)記錄當前拖拽的百分比pullingPercent。2)根據(jù)當前offset和臨界值更新當前狀態(tài)。
  • 如果是松手且處于pulling狀態(tài),則執(zhí)行刷新
  • 如果是松手且處于idle狀態(tài),則記錄拖拽的百分比pullingPercent
根據(jù)狀態(tài)的改變執(zhí)行一些操作
- (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];
        
        // 恢復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;
                // 增加滾動區(qū)域top
                self.scrollView.mj_insetT = top;
                // 設置滾動位置
                CGPoint offset = self.scrollView.contentOffset;
                offset.y = -top;
                [self.scrollView setContentOffset:offset animated:NO];
            } completion:^(BOOL finished) {
                [self executeRefreshingCallback];
            }];
         });
    }
}
從Refreshing轉為idle(說明刷新剛結束)
  • 用當前的時間更新lastUpdatedTimeKey
  • 執(zhí)行動畫,恢復inset,調整透明度(這里不用設置offset是因為從Refreshing轉為idle,如果offset大于originInset,自然會彈回。否則就意味著在刷新的過程中用戶向上滾動了,就保持當前的offset即可)
  • 動畫執(zhí)行完畢時將pullingPercent置為0,并執(zhí)行endRefreshingCompletionBlock
從其他狀態(tài)轉為Refreshing
  • 執(zhí)行動畫,設置inset為初始inset加上頭部控件高度,設置offset,滾動到臨界值(之所以要設置offset,是因為Refreshing狀態(tài)是可以通過代碼主動調用的,所以可以從任何狀態(tài)進入Refreshing狀態(tài)。此時不論offset為何值,都應該滾動到臨界值)
  • 動畫執(zhí)行完畢后執(zhí)行executeRefreshingCallback

MJRefreshFooter

主要職能

  • 提供構造方法
  • 通過覆蓋父類的方法設置一些上拉加載的基礎屬性

主要模塊

構造方法
#pragma mark - 構造方法
+ (instancetype)footerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
    MJRefreshFooter *cmp = [[self alloc] init];
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}
+ (instancetype)footerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    MJRefreshFooter *cmp = [[self alloc] init];
    [cmp setRefreshingTarget:target refreshingAction:action];
    return cmp;
}
覆蓋父類方法
#pragma mark - 重寫父類的方法
- (void)prepare
{
    [super prepare];
    
    // 設置自己的高度
    self.mj_h = MJRefreshFooterHeight;
    
    // 默認不會自動隱藏
    self.automaticallyHidden = NO;
}

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    if (newSuperview) {
        // 監(jiān)聽scrollView數(shù)據(jù)的變化
        if ([self.scrollView isKindOfClass:[UITableView class]] || [self.scrollView isKindOfClass:[UICollectionView class]]) {
            [self.scrollView setMj_reloadDataBlock:^(NSInteger totalDataCount) {
                if (self.isAutomaticallyHidden) {
                    self.hidden = (totalDataCount == 0);
                }
            }];
        }
    }
}
  • willMoveToSuperview里對tableview和collectionview的reloadData方法進行了動態(tài)交換,如果設置了自動隱藏,執(zhí)行reloadData時,上拉控件會在沒有數(shù)據(jù)時被隱藏。

MJRefreshBackFooter

主要職能

  • 重寫父類方法實現(xiàn)會回彈的上拉刷新功能

主要模塊

初始化
- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    [self scrollViewContentSizeDidChange:nil];
}
  • 設置底部控件的位置
監(jiān)聽scrollview的尺寸變化
- (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);
}
  • 根據(jù)scrollview的可視區(qū)域和contentSize設置底部控件的位置
監(jiān)聽scrollview的contentOffset
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    // 如果正在刷新,直接返回
    if (self.state == MJRefreshStateRefreshing) return;
    
    _scrollViewOriginalInset = self.scrollView.mj_inset;
    
    // 當前的contentOffset
    CGFloat currentOffsetY = self.scrollView.mj_offsetY;
    // 尾部控件剛好出現(xiàn)的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) {
            // 轉為即將刷新狀態(tài)
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && currentOffsetY <= normal2pullingOffsetY) {
            // 轉為普通狀態(tài)
            self.state = MJRefreshStateIdle;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
        // 開始刷新
        [self beginRefreshing];
    } else if (pullingPercent < 1) {
        self.pullingPercent = pullingPercent;
    }
}
  • 與header相似,根據(jù)currentOffsetY和happenOffsetY設置state
根據(jù)狀態(tài)的改變執(zhí)行一些操作
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根據(jù)狀態(tài)來設置屬性
    if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
        // 刷新完畢
        if (MJRefreshStateRefreshing == oldState) {
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                self.scrollView.mj_insetB -= self.lastBottomDelta;
                
                // 自動調整透明度
                if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
            } completion:^(BOOL finished) {
                self.pullingPercent = 0.0;
                
                if (self.endRefreshingCompletionBlock) {
                    self.endRefreshingCompletionBlock();
                }
            }];
        }
        
        CGFloat deltaH = [self heightForContentBreakView];
        // 剛刷新完畢
        if (MJRefreshStateRefreshing == oldState && deltaH > 0 && self.scrollView.mj_totalDataCount != self.lastRefreshCount) {
            self.scrollView.mj_offsetY = self.scrollView.mj_offsetY;
        }
    } else if (state == MJRefreshStateRefreshing) {
        // 記錄刷新前的數(shù)量
        self.lastRefreshCount = self.scrollView.mj_totalDataCount;
        
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
            CGFloat bottom = self.mj_h + self.scrollViewOriginalInset.bottom;
            CGFloat deltaH = [self heightForContentBreakView];
            if (deltaH < 0) { // 如果內容高度小于view的高度
                bottom -= deltaH;
            }
            self.lastBottomDelta = bottom - self.scrollView.mj_insetB;
            self.scrollView.mj_insetB = bottom;
            self.scrollView.mj_offsetY = [self happenOffsetY] + self.mj_h;
        } completion:^(BOOL finished) {
            [self executeRefreshingCallback];
        }];
    }
}
剛刷新完畢
  • 執(zhí)行動畫回置inset并自動調整透明度。動畫執(zhí)行完畢時將pullingPercent置為0,并執(zhí)行endRefreshingCompletionBlock。
  • 如果scrollView的內容超出scrollView的高度,并且有新數(shù)據(jù)增加,則調用scrollViewContentOffsetDidChange(猜測是為了處理在刷新時用戶拖拽著scrollview,刷新完畢時還處于拖拽狀態(tài),但沒有移動,為此要主動觸發(fā)一次scrollViewContentOffsetDidChange)
進入刷新狀態(tài)
  • 執(zhí)行動畫,設置inset和offset。動畫執(zhí)行完畢,執(zhí)行executeRefreshingCallback。

MJRefreshAutoFooter

主要職能

  • 重寫父類方法實現(xiàn)不回彈的上拉刷新功能

主要模塊

初始化
- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    if (newSuperview) { // 新的父控件
        if (self.hidden == NO) {
            self.scrollView.mj_insetB += self.mj_h;
        }
        
        // 設置位置
        self.mj_y = _scrollView.mj_contentH;
    } else { // 被移除了
        if (self.hidden == NO) {
            self.scrollView.mj_insetB -= self.mj_h;
        }
    }
}

- (void)prepare
{
    [super prepare];
    
    // 默認底部控件100%出現(xiàn)時才會自動刷新
    self.triggerAutomaticallyRefreshPercent = 1.0;
    
    // 設置為默認狀態(tài)
    self.automaticallyRefresh = YES;
    
    // 默認是當offset達到條件就發(fā)送請求(可連續(xù))
    self.onlyRefreshPerDrag = NO;
}
  • 因為不回彈,所以scrollview的inset.bottom需要加上底部控件的高度
  • 如果automaticallyRefresh = false,scrollViewContentOffsetDidChange內不執(zhí)行。
監(jiān)聽scrollview的尺寸變化
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change
{
    [super scrollViewContentSizeDidChange:change];
    
    // 設置位置
    self.mj_y = self.scrollView.mj_contentH;
}
  • 因為不回彈,所以即使內容不滿一個屏幕,底部控件也直接跟在內容下面成為內容的一部分。
監(jiān)聽scrollview的contentOffset
- (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) {
            // 防止手松開時連續(xù)調用
            CGPoint old = [change[@"old"] CGPointValue];
            CGPoint new = [change[@"new"] CGPointValue];
            if (new.y <= old.y) return;
            
            // 當?shù)撞克⑿驴丶耆霈F(xiàn)時,才刷新
            [self beginRefreshing];
        }
    }
}
  • 如果當前不處于idle狀態(tài),或者automaticallyRefresh為false,或者內容高度為0,或者內容高度少于一個屏幕,則什么也不做(這些情況的事都在scrollViewPanStateDidChange里做)
  • 這里offset是否觸發(fā)刷新操作的判斷條件比較復雜,要考慮到scrollview的內容高度包含了底部控件的高度,要把底部控件的高度減掉才是真正的內容高度。然后將內容高度加上底部控件可觸發(fā)刷新的高度,再減去scrollview的高度和scrollview的inset.bottom,就是可觸發(fā)刷新的offset。
監(jiān)聽scrollview的scrollViewPanStateDidChange
- (void)scrollViewPanStateDidChange:(NSDictionary *)change
{
    [super scrollViewPanStateDidChange:change];
    
    if (self.state != MJRefreshStateIdle) return;
    
    UIGestureRecognizerState panState = _scrollView.panGestureRecognizer.state;
    if (panState == UIGestureRecognizerStateEnded) {// 手松開
        if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h) {  // 不夠一個屏幕
            if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { // 向上拽
                [self beginRefreshing];
            }
        } else { // 超出一個屏幕
            if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView.mj_insetB - _scrollView.mj_h) {
                [self beginRefreshing];
            }
        }
    } else if (panState == UIGestureRecognizerStateBegan) {
        self.oneNewPan = YES;
    }
}

- (void)beginRefreshing
{
    if (!self.isOneNewPan && self.isOnlyRefreshPerDrag) return;
    
    [super beginRefreshing];
    
    self.oneNewPan = NO;
}
  • 如果內容小于一個屏幕,只要向上拽,松手后就刷新
  • 如果內容大于一個屏幕,拖拽到底部控件剛好全部露出時,松手后刷新(mj_contentH包含底部控件高度)
根據(jù)狀態(tài)的改變執(zhí)行一些操作
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    if (state == MJRefreshStateRefreshing) {
        [self executeRefreshingCallback];
    } else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
        if (MJRefreshStateRefreshing == oldState) {
            if (self.endRefreshingCompletionBlock) {
                self.endRefreshingCompletionBlock();
            }
        }
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容