最近項目需要添加刷新交互,想著參考一下別人的源碼再封裝,然后就找了MJRefresh,然后記錄下。
- 原理:
在
UIScrollView可滾動區(qū)域的頂部上方或底部下方加一個UIView,下拉時候監(jiān)聽偏移量,改變UIView的內(nèi)容顯示。利用
UIScrollView的contentInset屬性,在上下拉刷新時提供了顯示UIView的額外位置。
先看個例子:
tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
NSLog(@"下拉刷新");
}];
- 源碼
- 首先創(chuàng)建分類使用
runtime關聯(lián)屬性:
@implementation UIScrollView (MJRefresh)
static const char MJRefreshHeaderKey = '\0';
- (void)setMj_header:(MJRefreshHeader *)mj_header
{
if (mj_header != self.mj_header) {
// 刪除舊的,添加新的
[self.mj_header removeFromSuperview];
if (mj_header) {
[self insertSubview:mj_header atIndex:0];
}
// 存儲新的
objc_setAssociatedObject(self, &MJRefreshHeaderKey,
mj_header, OBJC_ASSOCIATION_RETAIN);//關聯(lián)屬性
}
}
- (MJRefreshHeader *)mj_header
{
return objc_getAssociatedObject(self, &MJRefreshHeaderKey);//關聯(lián)屬性
}
- 然后初始化刷新控件:
@implementation MJRefreshHeader
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentAction)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;
}
- 控件通過繼承來拓展功能:
@interface MJRefreshNormalHeader : MJRefreshStateHeader
@interface MJRefreshStateHeader : MJRefreshHeader
@interface MJRefreshHeader : MJRefreshComponent
@interface MJRefreshComponent : UIView
下面是繼承結構與作用:

- 核心主要看
KVO操作,當被添加進UIScrollView時添加監(jiān)聽:
@implementation MJRefreshComponent
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 如果不是UIScrollView,不做任何事情
if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
// 舊的父控件移除監(jiān)聽
[self removeObservers];
if (newSuperview) { // 新的父控件
...
// 添加監(jiān)聽
[self addObservers];
}
}
/*
NSString *const MJRefreshKeyPathContentOffset = @"contentOffset";
NSString *const MJRefreshKeyPathContentSize = @"contentSize";
NSString *const MJRefreshKeyPathPanState = @"state";
*/
- (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];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(i18nDidChange) name:MJRefreshDidChangeLanguageNotification object:MJRefreshConfig.defaultConfig];
}
- (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];
}
}
//監(jiān)聽調用的方法由子類重寫
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
- 當上下拉刷新時,監(jiān)聽
contentOffset更新狀態(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
};
@implementation MJRefreshHeader
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing狀態(tài)
if (self.state == MJRefreshStateRefreshing) {
[self resetInset];
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;
}
}
當往下拉時,狀態(tài)為MJRefreshStateIdle,一直往下拉超過臨界點,狀態(tài)變更為MJRefreshStatePulling,松手后判斷此前狀態(tài)為MJRefreshStatePulling時,代表要進行刷新操作,狀態(tài)變?yōu)?code>MJRefreshStateRefreshing:
@implementation MJRefreshComponent
- (void)beginRefreshing
{
...
// 只要正在刷新,就完全顯示
if (self.window) {
self.state = MJRefreshStateRefreshing;
} else {
// 預防正在刷新中時,調用本方法使得header inset回置失敗
if (self.state != MJRefreshStateRefreshing) {
self.state = MJRefreshStateWillRefresh;
// 刷新(預防從另一個控制器回到這個控制器的情況,回來要重新刷新一下)
[self setNeedsDisplay];
}
}
}
- 當狀態(tài)變化時,根據(jù)狀態(tài)做事情:
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根據(jù)狀態(tài)做事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;
[self headerEndingAction];
} else if (state == MJRefreshStateRefreshing) {
[self headerRefreshingAction];
}
}
- (void)headerEndingAction {
...
if (!self.isCollectionViewAnimationBug) {
// 恢復inset和offset
[UIView animateWithDuration:self.slowAnimationDuration animations:^{
self.scrollView.mj_insetT += self.insetTDelta;//修改contentInset,隱藏刷新控件
...
} completion:^(BOOL finished) { ... }];
return;
}
...
}
- (void)headerRefreshingAction {
if (!self.isCollectionViewAnimationBug) {
MJRefreshDispatchAsyncOnMainQueue({// 異步主線程執(zhí)行,不強持有Self
[UIView animateWithDuration:self.fastAnimationDuration animations:^{
if (self.scrollView.panGestureRecognizer.state != UIGestureRecognizerStateCancelled) {
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
// 增加滾動區(qū)域top
self.scrollView.mj_insetT = top;//修改contentInset,顯示出刷新控件
// 設置滾動位置
CGPoint offset = self.scrollView.contentOffset;
offset.y = -top;
[self.scrollView setContentOffset:offset animated:NO];//滾動到最頂部
}
} completion:^(BOOL finished) {
[self executeRefreshingCallback];//觸發(fā)回調
}];
})
return;
}
...
}
@implementation UIScrollView (MJExtension)
- (void)setMj_insetT:(CGFloat)mj_insetT
{
UIEdgeInsets inset = self.contentInset;
inset.top = mj_insetT;
#ifdef __IPHONE_11_0
if (respondsToAdjustedContentInset_) {
inset.top -= (self.adjustedContentInset.top - self.contentInset.top);
}
#endif
self.contentInset = inset;
}
@implementation MJRefreshComponent
- (void)executeRefreshingCallback
{
MJRefreshDispatchAsyncOnMainQueue({
if (self.refreshingBlock) {
self.refreshingBlock();
}
if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
}
if (self.beginRefreshingCompletionBlock) {
self.beginRefreshingCompletionBlock();
}
})
}
到目前為止完成了刷新的調用,當刷新結束后,把狀態(tài)變回MJRefreshStateIdle就完成一個刷新操作了。
- 但是還有兩個監(jiān)聽還沒看到,監(jiān)聽
contentSize用來更新footer的位置:
@implementation MJRefreshBackFooter
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change
{
[super scrollViewContentSizeDidChange:change];
// 內(nèi)容的高度
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);
}
@implementation UIView (MJExtension)
- (void)setMj_y:(CGFloat)mj_y
{
CGRect frame = self.frame;
frame.origin.y = mj_y;
self.frame = frame;
}
監(jiān)聽scrollView.panGestureRecognizer的state只用來更新MJRefreshAutoFooter這一個類的狀態(tài):
@implementation MJRefreshAutoFooter
- (void)scrollViewPanStateDidChange:(NSDictionary *)change
{
...
switch (panState) {
// 手松開
case UIGestureRecognizerStateEnded:
if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h) { // 不夠一個屏幕
if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { // 向上拽
self.triggerByDrag = YES;
[self beginRefreshing];
}
} else { // 超出一個屏幕
if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView.mj_insetB - _scrollView.mj_h) {
self.triggerByDrag = YES;
[self beginRefreshing];
}
}
break;
case UIGestureRecognizerStateBegan:
[self resetTriggerTimes];
break;
default:
break;
}
}
簡單來說,要封裝刷新控件核心是監(jiān)聽
UIScrollView的contentOffset進行更新界面與觸發(fā)回調,以及監(jiān)聽UIScrollView的contentSize來更新控件位置。