MJRefresh源码解读(一)

作者: iOS俱哥 | 来源:发表于2017-11-25 14:11 被阅读25次

    本篇先带着问题来看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的图解辨别

    相关文章

      网友评论

        本文标题:MJRefresh源码解读(一)

        本文链接:https://www.haomeiwen.com/subject/cowqbxtx.html