牛叉的demo恩美第二个APP项目程序员

MJRefresh源码解读(一)

2017-11-25  本文已影响25人  iOS俱哥

本篇先带着问题来看MJRefresh,在下拉时MJRefresh是怎么使箭头旋转,又是如何使菊花(或其他动画图片)停留一段时间的呢?效果看下图。

截图一.gif
于是乎我对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的刷新机制是流畅和完善的,并且是连续的完成刷新事件。在几个刷新控件的的功能实现上各有分工,使得该开源库读起来简洁并且有调理,用起来也较为方便。

参考链接:contentSize、contentOffset和contentInset的图解辨别

上一篇 下一篇

猜你喜欢

热点阅读