MJRefresh源码分析 下拉刷新
MJRefresh
是李明杰老师的一个开源项目,GitHub目前已经有10000多star,GitHub地址是MJRefresh
下面我们一起来分析下MJRefresh框架的实现过程。
-
MJRefresh中类与类之间的联系
mjrefresh.png - 从我们使用MJRefresh框架的调用代码分析
eg:
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
// 属性中的回调
}];
[self.tableView.mj_header beginRefreshing];
上面的代码会调用MJRefreshNormalHeader
父类MJRefreshStateHeader
的父类MJRefreshHeader
的方法:
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
// 实例化MJRefreshHeader的对象
MJRefreshHeader *cmp = [[self alloc] init];
// refreshingBlock 父类的属性,把refreshingBlock赋值cmp.refreshingBlock属性
cmp.refreshingBlock = refreshingBlock;
return cmp;
}
上面的headerWithRefreshingBlock:refreshingBlock;
方法实例化一个一个对象cmp,会触发MJRefreshHeader
父类中的- (instancetype)initWithFrame:(CGRect)frame
的方法。
#pragma mark - 初始化方法
- (instancetype)initWithFrame:(CGRect)frame
{
// 注意,此时的self 是 MJRefreshNormalHeader的对象,为什么是 MJRefreshNormalHeader的对象,设计到继承的知识点,可以具体参考继承,这里就不过多的说明
if (self = [super initWithFrame:frame]) {
// 调用 MJRefreshNormalHeader 中prepare方法
[self prepare];
// 默认是普通状态,调用MJRefreshNormalHeadersetState方法
self.state = MJRefreshStateIdle;
}
return self;
}
我们回到MJRefreshNormalHeader类中的prepare方法,方法具体实现如下
#pragma mark - 重写父类的方法
- (void)prepare
{
// 调用父类的 prepare 父类 是 MJRefreshStateHeader
[super prepare];
// 设置菊花样式
self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
}
此时又会去调用MJRefreshNormalHeader父类MJRefreshStateHeader中的prepare的方法
- (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];
}
然后又会去调用父类中的prepare
的方法,直到MJRefreshComponent
类中的prepare
的方法执行完毕。关于prepare
方法,里面都是做一些初始化和frame
的设置,比较简单,就不具体分析了。
再回到最开始的方法
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
// 属性中的回调
}];
把MJRefreshNormalHeader
视图赋值给mj_header
,mj_header
是UIScrollView+MJRefresh
类中的属性,要给分类添加属性,就要用到runtime机制,具体代码如下:
#pragma mark - header
static const char MJRefreshHeaderKey = '\0';
- (void)setMj_header:(MJRefreshHeader *)mj_header
{
if (mj_header != self.mj_header) {
// 删除旧的,添加新的
[self.mj_header removeFromSuperview];
// A insertSubView B AtIndex:2 是将B插入到A的子视图index为2的位置(最底下是0)
// eg [self addsuview: mj_header];
[self insertSubview:mj_header atIndex:0];
// 手动kvo
[self willChangeValueForKey:@"mj_header"]; // KVO
// 给分类中的属性添加一个set方法....
// 分类能添加属性。但是不会自己生成getter和setter方法
objc_setAssociatedObject(self, &MJRefreshHeaderKey,
mj_header, OBJC_ASSOCIATION_ASSIGN);
// 手动kvo
[self didChangeValueForKey:@"mj_header"]; // KVO
}
}
// get方法
- (MJRefreshHeader *)mj_header
{
return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}
setMj_header
的方法中,监听属性用了iOS 的设计模式 KVO
[self willChangeValueForKey:@"mj_header"]; // KVO
[self didChangeValueForKey:@"mj_header"]; // KVO
为什么要用 willChangeValueForKey
和didChangeValueForKey
方法监听分类的中的属性,而不是- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
具体可以参考KVO的在分类中的用法
[self insertSubview:mj_header atIndex:0];
A insertSubView B AtIndex:0
是将B插入到A的子视图index
为0的位置。
1、这句代码会触发MJRefreshComponent
类中的- (void)willMoveToSuperview:(nullable UIView *)newSuperview;
此方法什么时候被调用?经过查资料得知:当视图即将加入父视图时或者当视图即将从父视图移除时调用,具体我们分析下此方法
// newSuperview 就是父视图 这里值得 uiscrollerView
- (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;
;
NSLog(@"contentInset:%@",NSStringFromUIEdgeInsets(_scrollView.contentInset));
// 添加监听
[self addObservers];
}
}
此方法中的 self.mj_w = newSuperview.mj_w;
self
就是下拉的展示出来的view
,mj_w
是UIView+MJExtension
中的属性,实现的set的方法- (void)setMj_w:(CGFloat)mj_w
,具体方法实现如下
- (void)setMj_w:(CGFloat)mj_w
{
CGRect frame = self.frame;
frame.size.width = mj_w;
self.frame = frame;
}
分析到这里,应该明白了self.mj_w = newSuperview.mj_w;
的意思了。self.mj_w = CGRectMake(original, original, newSuperview.mj_w, original);
[self addObservers];
用KVO添加监听,给当前的UIScrollView添加了contentOffset
、contentSize
、panGestureRecognizer
的监听
2、 [self insertSubview:mj_header atIndex:0];此方法还会触发MJRefreshComponent
类中layoutSubviews
方法,触发 layoutSubviews
有哪些操作?
找了下资料并总结下:
1、调用 addSubview 方法时会执行该方法
2、设置并改变视图的frame属性时会触发该方法
3、滑动UIScrollView及继承与UIScrollView的控件时会触发该方法
4、旋转屏幕时,会触发父视图的layoutSubviews方法、设置并改变视图的frame属性时会触发父视图的layoutSubviews方法
OK,咱们一起看看MJRefreshComponent
类中的layoutSubviews
方法
- (void)layoutSubviews
{
// 此处的self依然是MJRefreshNormalHeader的对象
[self placeSubviews];
[super layoutSubviews];
}
MJRefreshNormalHeader
类中的placeSubviews
添加了两个视图arrowView
(箭头视图)、loadingView
(菊花视图)
MJRefreshStateHeader
类中的placeSubviews
添加了两个视图stateLabel
(状态label )、lastUpdatedTimeLabel
(显示时间label)
MJRefreshHeader
类中的placeSubviews
添加了设置了当前视图的Y
坐标
MJRefreshComponent
类中的placeSubviews
没有干啥 ☺
鉴于placeSubviews方法比较简单,都是关于界面的搭建,再次就不多多啰嗦了。
OK,分析到这里界面啥的都出来了。下面具体分析下拉的视图如何出现
由于监听了UIScrollView
的 contentOffset
属性,当我们下拉的时候,触发监听方法。监听方法在MJRefreshHeader类中
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing状态
if (self.state == MJRefreshStateRefreshing) {
if (self.window == nil) return;
NSLog(@"%@",NSStringFromCGPoint(self.scrollView.contentOffset));
// sectionheader停留解决
//- self.scrollView.mj_offsetY:-(-54)= 54 : 刷新的时候,偏移量是不动的。偏移量 = 状态栏 + 导航栏 + header的高度
//_scrollViewOriginalInset.top:64 (状态栏 + 导航栏)
//insetT 取二者之间大的那一个
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;
}
NSLog(@"scrollViewContentOffsetDidChange");
// 跳转到下一个控制器时,contentInset可能会变
_scrollViewOriginalInset = self.scrollView.contentInset;
// 当前的contentOffset Y
CGFloat offsetY = self.scrollView.mj_offsetY;
// 头部控件刚好出现的offsetY
CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
// 如果是向上滚动到看不见头部控件,直接返回
// >= -> >
// 解释下: offsetY 正值 就是上滑动
// offsetY 负值 就是下拉
if (offsetY > happenOffsetY) return;
// 从普通 到 即将刷新 的临界距离 normal2pullingOffsetY = -54
CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
//下拉的百分比:下拉的距离与header高度的比值
CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
if (self.scrollView.isDragging) { // 如果正在拖拽
self.pullingPercent = pullingPercent;
if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
// 如果当前为默认状态 && 下拉的距离大于临界距离(将tableview下拉得很低),则将状态切换为可以刷新
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;
}
}
根据不同的state
展示界面
MJRefreshStateHeader
中的setState
方法
- (void)setState:(MJRefreshState)state
{
// MJRefreshCheckState
// 状态检查
//#define MJRefreshCheckState \
MJRefreshState oldState = self.state;
if (state == oldState) return;
[super setState:state];
// 设置状态文字
self.stateLabel.text = self.stateTitles[@(state)];
// 重新设置key(重新显示时间)
self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}
MJRefreshNormalHeader中的setState方法
- (void)setState:(MJRefreshState)state
{
MJRefreshState oldState = self.state;
if (state == oldState) return;
[super setState:state];
// 根据状态做事情
if (state == MJRefreshStateIdle) {
if (oldState == MJRefreshStateRefreshing) {
// 现在的状态是 MJRefreshStateIdle ,上一个状态时 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 {
// 当它停止的时候,菊花视图就会自动隐藏。
// loadingView.hidesWhenStopped = YES;
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowView.transform = CGAffineTransformIdentity;
}];
}
} else if (state == MJRefreshStatePulling) {
// loadingView 就是菊花的视图
[self.loadingView stopAnimating];
// 箭头视图
self.arrowView.hidden = NO;
// 让箭头旋转180°
[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;
}
}
MJRefreshStateHeader
中的setState
方法
- (void)setState:(MJRefreshState)state
{
// MJRefreshCheckState
// 状态检查
//#define MJRefreshCheckState \
MJRefreshState oldState = self.state;
if (state == oldState) return;
[super setState:state];
// 设置状态文字
self.stateLabel.text = self.stateTitles[@(state)];
// 重新设置key(重新显示时间)
self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}
MJRefreshNormalHeader
中的setState
方法
- (void)setState:(MJRefreshState)state
{
// MJRefreshCheckState
// 状态检查
//#define MJRefreshCheckState \
MJRefreshState oldState = self.state;
if (state == oldState) return;
[super setState:state];
// 根据状态做事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;
// 当前的状态必须是 MJRefreshStateIdle ,上一个状态是 MJRefreshStateRefreshing,才可以保存时间和恢复uiscrollerView的 inset和 offset
// 保存刷新时间
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
[[NSUserDefaults standardUserDefaults] synchronize];
NSLog(@"MJRefreshState");
// 恢复inset和offset
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.scrollView.mj_insetT += self.insetTDelta;
NSLog(@"%@",NSStringFromUIEdgeInsets(self.scrollView.contentInset));
// 自动调整透明度
if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
} completion:^(BOOL finished) {
self.pullingPercent = 0.0;
if (self.endRefreshingCompletionBlock) {
self.endRefreshingCompletionBlock();
}
}];
}
else if (state == MJRefreshStateRefreshing) {
// 对UI的调度,都应该在主线程中
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];
}];
});
}
}
MJRefreshComponent
中的setState
方法
- (void)setState:(MJRefreshState)state
{
_state = state;
// 加入主队列的目的是等setState:方法调用完毕、设置完文字后再去布局子控件
dispatch_async(dispatch_get_main_queue(), ^{
[self setNeedsLayout];
});
}
关于下拉刷新,分析就到此为止,更多用法,参考MJRefreshDemo