MJRefresh是Github上点赞次数最多的刷新控件,本文主要演示如何使用MJRefresh实现刷新和MJRefresh的内部实现原理。
1.使用MJRefresh实现刷新——UIScrollView+MJRefresh.h
使用MJRefresh实现刷新非常简单,主要分3步:1.导入框架。2.定义一个Scrollview;2.为Scrollview添加刷新控件。
// 1.导入框架
#import "MJRefresh.h"
@interface ViewController()
@property (nonatomic, strong) UITableView * tableView;
@end;
@implementation ViewController;
- (void)viewDidLoad
{
[super viewDidLoad];
// 2.定义一个UITableView
self.tableView = [[UITableView alloc] initWithFrame: self.view.bounds];
[self.view addSubview:self.tableView];
// 3.添加刷新控件
// 下拉刷新
__weak typeof(self) weakSelf = self;
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
// 模拟延迟加载数据,因此2秒后才调用(真实开发中,可以移除这段gcd代码)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 结束刷新
[weakSelf.tableView.mj_header endRefreshing];
});
}];
// 上拉刷新
self.tableView.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
// 模拟延迟加载数据,因此2秒后才调用(真实开发中,可以移除这段gcd代码)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 结束刷新
[weakSelf.tableView.mj_footer endRefreshing];
});
}];
}
MJRefresh定义了一个类别UIScrollView+MJRefresh.h,利用类别为Tableview增加了两个属性mj_header和mj_footer。
#import <UIKit/UIKit.h>
#import "MJRefreshConst.h"
@class MJRefreshHeader, MJRefreshFooter;
@interface UIScrollView (MJRefresh)
/** 下拉刷新控件 */
@property (strong, nonatomic) MJRefreshHeader *mj_header;
/** 上拉刷新控件 */
@property (strong, nonatomic) MJRefreshFooter *mj_footer;
@end
由于在类别中添加属性,并不能在编译期间自动添加成员变量、set和get方法(因为类的结构已经确定,在类别中再添加成员变量会影响已经添加的成员变量的存储。),所以我们要objc_setAssociatedObject和objc_getAssociatedObject的两个方法自己实现。
#pragma mark - header
static const char MJRefreshHeaderKey = '\0';
- (void)setMj_header:(MJRefreshHeader *)mj_header
{
if (mj_header != self.mj_header) {
// 删除旧的,添加新的
[self.mj_header removeFromSuperview];
[self insertSubview:mj_header atIndex:0];
// 存储新的
[self willChangeValueForKey:@"mj_header"]; // KVO
objc_setAssociatedObject(self, &MJRefreshHeaderKey,
mj_header, OBJC_ASSOCIATION_ASSIGN);
[self didChangeValueForKey:@"mj_header"]; // KVO
}
}
- (MJRefreshHeader *)mj_header
{
return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}
MJRefreshHeaderKey这是定义了一个静态字符,用它的地址作为存储mj_header的key,用最小的存储空间实现了key的定义。
通过上文中的代码我们可以看出,所谓的添加下拉刷新控件就是在Scrollview上加了一个View。下文我们将介绍MJRefresh是怎么定义这个View(mj_header)的。
2.继承关系
MJRefresh总共分四层,我将从最底层MJRefreshComponent开始介绍MJRefresh的实现原理。

3.MJRefreshComponent
从这个类我们可以知道,MJRefreshHeader的实现主要是利用KVO监听ScrollView的contentOffset,contentInset,contentSize三个属性的变化来确定ScrollView的刷新状态(MJRefreshState)。在- (void)setState:(MJRefreshState)state中实现ScrollView的各个状态下的UI变化和调用回调方法。
// 当刷新空间将要添加到父视图中/从父视图中移除
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 如果不是UIScrollView,不做任何事情
if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
// 旧的父控件移除监听
[self removeObservers];
// 添加到父视图
// 如果newSuperview为nil,表示从父视图中移除
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;
// 添加监听
[self addObservers];
}
}
#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];
}
}
// 当ContentOffset变化的时候调用
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
// 当ContentSize变化的时候调用
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
- (void)willMoveToSuperview:(nullable UIView *)newSuperview;这个方法在View添加到父视图(addSubView)和从父视图中移除(removeFromSuperView)都会调用。当newSuperview存在的时候,代表的是添加到父视图,此时添加KVO,并为每个属性设置了回调方法。 MJRefreshComponent的子类MJRefreshHeader实现- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change;,根据ContentOffset的变化来确定ScrollView的刷新状态。所以,我们研究MJRefresh刷新的实现,就研究 MJRefreshComponent的各个子类在这个方法中的实现即可。
此外还定义了两个方法- (void)prepare;和- (void)placeSubviews;,都需要在子类中实现。在- (void)prepare;方法中,主要是进行数据的初始化,- (void)placeSubviews;中主要是实现控件的摆放,- (void)prepare;先于- (void)placeSubviews;执行。
4.MJRefreshHeader
在MJRefreshHeader中实现了- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change方法,确定了ScrollView的刷新状态,就是确定了这个属性@property (assign, nonatomic) MJRefreshState state;的值。
分两种情况,一种是ScrollView正在刷新(MJRefreshStateRefreshing),通过修改ScrollView的contentInset,增加了contentInset.top的值,增加了MJRefreshHeader的高度,让MJRefreshHeader完全显示出来,以达到Hearder悬停效果;另一种是其他状态,此时来判断什么时候开始刷新。

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 此时ScrollView正在刷新
if (self.state == MJRefreshStateRefreshing) {
// 如果视图还没有添加到KeyWindow上,返回。
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;
// 头部控件刚好出现的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;
}
}
MJRefreshComponent的每个子类都实现了- (void)setState:(MJRefreshState)state这个方法。MJRefreshHeader实现了通过改变和恢复inset和offset来实现Scrollview的刷新效果。当state == MJRefreshStateIdle的时候,恢复inset。当state == MJRefreshStateRefreshing的时候,增加inset.top一个MJRefreshHeader的高度,并且滚动到顶部。
- (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;
// 设置滚动位置
CGPoint offset = self.scrollView.contentOffset;
offset.y = -top;
[self.scrollView setContentOffset:offset animated:NO];
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
});
}
}
这个类还定义了两个构造方法,在构造方法中保存了头部刷新的回调。
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock;
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action;
5.MJRefreshStateHeader
设置lastUpdatedTimeLabel和stateLabel的位置以及显示的内容。这个类非常简单,没有什么可讲的。这个类里的- (void)setState:(MJRefreshState)state;实现的是这两个Label显示的内容。
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 设置状态文字
self.stateLabel.text = self.stateTitles[@(state)];
// 重新设置key(重新显示时间)
self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}

6. MJRefreshNormalHeader(MJRefreshGifHeader)
MJRefreshNormalHeader刷新的时候是一个菊花,MJRefreshGifHeader可以自定义MJRefreshHeader各个状态的动画,其实两个的实现思路大同小异,只是一个是UIActivityIndicatorView,一个是UIImageView,两者的位置都是相同的。剩下的就是动画效果的是实现了,这应该属于最基本的动画了,在这里我就不讲了,有不懂的可以自行百度。
此外还有一个箭头(arrowView),这个箭头就是UIImageView,它和UIActivityIndicatorView(Gif)交替展示,就是一个隐藏,一个就显示。这个类里的- (void)setState:(MJRefreshState)state;实现的是arrowView和loadingView的动画效果。
- (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;
}
}
MJRefresh的分层非常清晰,一目了然,一些个功能的实现非常巧妙,而且大部分还有注释,这是了解UIScrollView以及刷新机制的非常好的一个框架。下一篇我将分析MJRefreshFooter。