首先看下結構圖
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