本篇先带着问题来看MJRefresh,在下拉时MJRefresh是怎么使箭头旋转,又是如何使菊花(或其他动画图片)停留一段时间的呢?效果看下图。
于是乎我对MJRefresh探究了一番,MJRefresh源码地址,查看MJRefresh在GitHub的介绍可以得知它的主要成员如下图:
MJRefresh集成关系.png
所有的刷新控件都是继承于基类MJRsfreshComponent的。第一步看看
MJRsfreshComponent.m
文件是怎么写的。
一.对scrollView对象添加监听:
#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];
}
可以看出对scrollView对象添加了KVO监听,当scrollView有滑动手势操作,contentOffset属性值有变化时,进行一些处理。使得有下拉箭头的变化和菊花的显示,那么具体是怎么实现操作的呢,咱接着分析。
二.查看对contentOffset的监听
先来了解一下什么是contentOffset:是scrollView基本的属性。
contentOffset:即偏移量,其中分为contentOffset.y=内容的顶部和frame顶部的差值,contentOffset.x=内容的左边和frame左边的差值。
其实对contentOffset进行监听就是看scrollView的内容是否有偏移的变化。
在MJRefreshComponent.m文件中实现了观察监听的方法。
- (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{}
方法。可以看到这个方法只是在基类MJRefreshComponent仅写了空方法,但是在MJRefreshHeader.m文件中具体实现了这个方法
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing状态
if (self.state == MJRefreshStateRefreshing) {
// sectionheader停留解决
...
return;
}
...
// 当前的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)scrollViewContentOffsetDidChange:(NSDictionary *)change
方法实现里,提现的是对state的状态的判断和改变,而对- (void)setState:(MJRefreshState)state
的实现,几个刷新空间都有自己的任务。
三.对state的改变做任务
在对MJRefreshComponent、MJRefreshHeader、MJRefreshStateHeader、MJRefreshNormalHeader四个文件查看是都有实现state的setter方法。不同的是后边的三个子类方法里都有MJRefreshCheckState
这个宏。对状态做了判断和调用父类的方法。
// 状态检查
#define MJRefreshCheckState \
MJRefreshState oldState = self.state; \
if (state == oldState) return; \
[super setState:state]; \
分别在三个子类中添加一个行打印方法的代码,如下:
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
NSLog(@"%s",__func__);//添加的打印方法
// 根据状态做事情
if (state == MJRefreshStateIdle) {
...
} else if (state == MJRefreshStateRefreshing) {
...
}
}
运行demo程序,打印结果如下:
setState.png可见这四个刷新控件会依次执行- (void)setState:(MJRefreshState)state
。
主要查看MJRefreshNormalHeader文件的- (void)setState:(MJRefreshState)state
方法。
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
NSLog(@"%s",__func__);
// 根据状态做事情
if (state == MJRefreshStateIdle) {//闲置状态
if (oldState == MJRefreshStateRefreshing) {//正在刷新中的状态
self.arrowView.transform = CGAffineTransformIdentity;
[...
} else {
...
}
} else if (state == MJRefreshStatePulling) {//松开就可以进行刷新的状态
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
NSLog(@"----transform");
}];
} else if (state == MJRefreshStateRefreshing) {//正在刷新中的状态
self.loadingView.alpha = 1.0; // 防止refreshing -> idle的动画完毕动作没有被执行
[self.loadingView startAnimating];
self.arrowView.hidden = YES;
}
}
四.解答疑问
分析到这里就可以解释文章开头抛出的问题了:
在下拉时MJRefresh是怎么使箭头旋转,又是如何使菊花(或其他动画图片)停留一段时间的呢?
1.由于拖拽scrollview使其contentOffset发生了变化
2.在监听contentOffset发生变化的方法里判断偏移量的变化
3.根据偏移量的变化来设置当前state的值,即对当前的刷新状态进行改变
4.根据state的变化来做箭头旋转和菊花(或其他动画)的展示
在第四步中,有更为详细的处理:利用UIView的+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion
来处理动画事件。
那么菊花(或其他动画)是如何保持一段时间然后消失的呢?
1.初始化header
在初始化tableView.mj_header
可以看到:
- (void)viewDidLoad
{
[super viewDidLoad];
...
// 下拉刷新
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(), ^{
// 结束刷新
[tableView.mj_header endRefreshing];
});
}];
...
}
在这里是模拟延迟加载数据,当有下拉动作并松手时时,菊花会一直显示,知道调用[tableView.mj_header endRefreshing]
。
2.调用- (void)endRefreshing
结束刷新事件
#pragma mark 结束刷新状态
- (void)endRefreshing
{
dispatch_async(dispatch_get_main_queue(), ^{
self.state = MJRefreshStateIdle;
});
}
是把当前的刷新状态state直接改变为普通闲置状态
3.state改变的处理事件
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
NSLog(@"%s",__func__);
// 根据状态做事情
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 {
...
} else if (state == MJRefreshStatePulling) {
...
} else if (state == MJRefreshStateRefreshing) {
...
}
}
总结:可以看出MJRefresh的刷新机制是流畅和完善的,并且是连续的完成刷新事件。在几个刷新控件的的功能实现上各有分工,使得该开源库读起来简洁并且有调理,用起来也较为方便。
网友评论