zoukankan      html  css  js  c++  java
  • MJRefresh 源码解读 + 使用

    MJRefresh这个刷新控件是一款非常好用的框架,我们在使用一个框架的同时,最好能了解下它的实现原理,不管是根据业务要求在原有的基础上修改代码,还是其他的目的,弄明白作者的思路和代码风格,会受益匪浅。

    前言

    随着开发经验的不断积累,个人的能力也会不断提高。每个人的进步都会有一个过程,这个过程就好比登山。一个框架对我们的意义从开始的简单实用,慢慢的就会过渡到了解,最终让它成为你大脑的一部分。这篇文章不会对代码进行逐行解读,最重要的目的是让朋友们明白 MJRefresh 是怎么做到刷新的,我们还能如何扩展它的功能。

    原理部分

    我们在 MJRefreshMJRefresh Github 上拿到了下边这张图:

    正如这张图片所说的,所有的功能的实现都基于 MJRefreshComponent 这个刷新组件。MJRefreshComponent 有两个分支 MJRefreshHeader(给了下拉刷新的能力) MJRefreshFooter(给了上拉刷新的能力)。

    在进行下边的代码分析之前,我们来看看刷新的整个过程(我们以头部刷新为例):

    1. 看这张图片上的代码,能够tableView.mj_header这样使用,说明tableview有mj_header这个属性。然后我们看一下UIScrollView+MJRefresh这个文件。原理是给这个scrollview添加了一个控件,代码如下
    2. 我看了下MJRefreshHeader的这个文件的头文件,对我们理解原理并没有帮助,由于它是继承MJRefreshComponent的,所以我们打开MJRefreshComponent的头文件来看。
    3. 这几个状态非常关键,是使用整个框架的核心思路。为什么这么说?如果我们在滑动的过程中,状态也会改变,那么必然会调用状态的setter方法,我们就利用这个方法,显示自己想要的效果。
    4. 到这里基本上就很明白了。由于MJRefreshComponent是一个UIView,因此我们可以随意往上添加控件,我们在prepare中添加控件,在placeSubviews中布局,通过scrollViewContentOffsetDidChange:方法指导contentOffset改变了。然后就能自定义事件了。
    5. 最终我们在我们最需要的状态上绑定事件就ok了。

    MJRefreshComponent

    MJRefreshComponent 这个类是最最基础能力的搭建了。除了暴露出业务接口外。值得注意的有两点:

    1. 上边UIScrollView+MJRefresh中提到把mj_header这个view加到了scrollview上,那么MJRefreshComponent是如何获取scrollview的呢?
    2. 如何监听我们需要的变化?比如说contentOffSet。。。

    示例代码:

    - (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;
            
            // 添加监听
            [self addObservers];
        }
    }
    

    当一个控价被添加到另一个控件上的时候,就会调用这个方法,这这个方法中我们就获取到了scrollview,并且设置了监听事件,监听事件这里就不写了。

    MJRefreshHeader

    MJRefreshHeader 这个类提供了刷新的核心功能,这个类并没有像UIImageview,UILabel这样的控件。所以说他同样是一个基础类,我们使用中只需继承这个类,添加需要的UI控件就行了。

    示例代码:

    - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
    {
        [super scrollViewContentOffsetDidChange:change];
        
        // 在刷新的refreshing状态
        if (self.state == MJRefreshStateRefreshing) {
            if (self.window == nil) return;
            
            // sectionheader停留解决
            NSLog(@"%f , %f",self.scrollView.mj_offsetY,_scrollViewOriginalInset.top);
            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.contentInset;
        
        // 当前的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;
        }
    }
    

    这个是通过contentOffSet计算状态的核心方法。由于代码注释很详细,就不做解释了,只要一行一行理解就能行了。

    MJRefreshAutoFooter

    MJRefreshAutoFooter 提供了当滑到底部时,自动加载的功能,我们来看看代码:

    - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
    {
        [super scrollViewContentOffsetDidChange:change];
        
        if (self.state != MJRefreshStateIdle || !self.automaticallyRefresh || self.mj_y == 0) return;
        
        if (_scrollView.mj_insetT + _scrollView.mj_contentH > _scrollView.mj_h) { // 内容超过一个屏幕
            // 这里的_scrollView.mj_contentH替换掉self.mj_y更为合理
            if (_scrollView.mj_offsetY >= _scrollView.mj_contentH - _scrollView.mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + _scrollView.mj_insetB - self.mj_h) {
                // 防止手松开时连续调用
                CGPoint old = [change[@"old"] CGPointValue];
                CGPoint new = [change[@"new"] CGPointValue];
                if (new.y <= old.y) return;
                
                // 当底部刷新控件完全出现时,才刷新
                [self beginRefreshing];
            }
        }
    }
    

    演示案例

    我们就简单演示 转转 的下拉加载效果。由于代码改动比较小,在这里下载:转转下拉效果

    总结

    有时间写一个类似知乎刷新那样的效果。

  • 相关阅读:
    BZOJ5212 ZJOI2018历史(LCT)
    BZOJ5127 数据校验
    253. Meeting Rooms II
    311. Sparse Matrix Multiplication
    254. Factor Combinations
    250. Count Univalue Subtrees
    259. 3Sum Smaller
    156. Binary Tree Upside Down
    360. Sort Transformed Array
    348. Design Tic-Tac-Toe
  • 原文地址:https://www.cnblogs.com/machao/p/5826817.html
Copyright © 2011-2022 走看看