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