MJRefresh记录

一天

背景

写这篇文章主要有两个目的:

  1. 虽然refresh的源码已经有很多小伙伴分析过了。但是其应用不能说不够广阔。所以,抽空重新梳理了一下该源码,然后记录一下自己的心得。方便之后重新阅读,然后对比更新。
  2. 另一个问题是发现一个阅读源码的现象:比如源码有100个文件,当阅读了10个文件,就说我阅读过某某某框架的源码,然后看到源码中某某方法和另一方法实现一致,然后就直接调用同一个方法。但是此时就很容易出现问题。有时和你理论的时候,依旧意识不到问题所在,继续翻开源码说实现是一致的。可源码整体看一下,就能发现遇到的问题。最后一种情况将会分析这遇到的问题。

本文从以下几个方面进行记录:

  • 继承层次
  • 整体分析
  • 各类的职责
  • 下拉刷新
    • 主动调用代码
    • UI进行交互
  • 上拉刷新
  • 常见的错误❌

一、基本类继承层次关系

// 为了方便表示,使用~代替
~ = MJRefresh

~Component: UIView
    ~Header
        ~StateHeader
            ~NormalHeader
            ~GifHeader
    ~Footer
        ~AutoFooter
            ~AutoStateFooter
                ~AutoNormalFooter   
                ~AutoGifFooter
        ~BackFooter 
            ~BackStateFooter
                ~BackNormalFooter
                ~BackGifFooter

二、整体分析:

  1. MJRefreshComponet是继承自UIView。其他所有的类都是其子类。
  2. mj_header和mj_footer是添加在ScrollView上分类的属性,其类型是Header/Footer类型,并且其添加到了scrollview的view容器内。因此在赋值后,才会出现在头部/尾部。
  3. 通篇控制的方式,是通过子类重写其基类的MJRefreshState类型的state属性,重写其set方法进行更改当前刷新的状态。从而控制头部和尾部的展示内容(是否隐藏,内边距,偏移量等)。最终汇聚成看到的展示样貌。

三、各类的职责

要看清各个部件是如何进行逻辑的组织及代码的编写,得分清各个类的职责是什么,负责哪一模块,然后才能更好地理通整个源码的思想。

  1. ~State模块主要处理状态:时间和不同状态的title
  2. ~Normal模块主要处理:箭头的方向,是否显示及loadingView的是否显示。
  3. ~Component基模块:主要暴露相应的接口,block供给子类进行组装及监听等的处理。
  4. ~Header 、~Footer模块:初始化的操作、设置frame的高度、scrollview的布局操作(inset, offset等)。

四、Header

下拉刷新刷新的方式有两种:一种是首次进入页面的时候,就主动触发代码刷新的操作,使其开始下拉刷新。另一种是手动下拉scrollview,进行的一系列UI操作及其给出的反馈。

主动调用代码进行的刷新操作

调用流程:

  • -[MJRefreshHeader beginRefreshing]

    1. setState:(refreshing)
    2. 展示刷新样式(增加滚动区域,更新offest值为header的高度)
    3. 执行block内部的刷新请求(executeRefreshingCallback)。
    4. 当3中的请求回调操作执行完之后,当外部主动调用- endRefreshing时进行还原操作。
  • -[MJRefreshHeader endRefreshing]

    1. setState:(idle)
    2. 恢复刷新样式,头部消失(更改inset为初始值)
    3. 如果endrefreshing中有回调时就执行,没有就结束整个流程。

与UI进行交互出现的刷新操作

主要有通过下拉或者上滑出现的UI变动及刷新的逻辑等。后半部分是一致的。主要分析前半部分与UI进行交互,然后进行更新头部的动画的过程。

以下拉刷新为例:(其中h为header的高度)

  1. 当下拉scrollview时,触发其delegate方法:scrollViewContentOffsetDidChange:方法。

  2. 当drag == true 时,state状态一直在变化;

    • 下拉到临界值:
      (state == idle && offsetY < -h)时 --> state = pulling。此时状态文字要发生变化,箭头也要进行翻转。根据各类的职责去相应的类中更改即可。

    • 上拉到临界值:
      state == pulling && offsetY >= -h时,恢复state = idle。同时恢复箭头,文字等的状态。

  3. 当drag == false && state == pulling,即在拉过临界值,并且松手了,那么此之后执行的逻辑,就是下拉刷新过程。


五、Footer

比Header包含的状态稍微多一点,但是和header的逻辑是类似的。
Footer分为Auto和Back两种状态

  • 联系:与header的比较,之前处理 ~Header的逻辑现在迁移到了AutoFooter和BackFooter。

  • 区别:~BackFooter的刷新是通过手动拖拽,并且在tableview的尾部最后进行展示的。而~AutoFooter的刷新操作,当上滑到最后一个cell时,将会自动进行刷新的操作。

对于footer通常不会去主动调用- beginRefresing该方法。他在设置好相应的初始位置后,就开始进行手动与UI
进行交互的上拉刷新操作:

~BackFooter:
  1. footer的位置在屏幕的下方(无论是contentsize.height是否大于screensize.height).
  2. 之后的操作就不再赘述,和下拉刷新的流程真的一模一样。(可以通过源码进行对照查看)
~AutoFooter:
  1. footer.y = contentsize.height + △ (其中△是一些inset信息).
  2. 之后的刷新也和下拉是一致的内容.

六、常见的错误❌:

  • 问题出现点:在上拉刷新的时候,根据有没有更多数据,来更改state。

  • 代码:

    if pullup {
        self.endFooterRefreshing(noMoreData: noMoreData)
    } 
    else {
         self.tableView.mj_header.endRefreshing()
    }

    private func endFooterRefreshing(noMoreData: Bool) {
        if noMoreData {
            self.tableView.mj_footer.endRefreshingWithNoMoreData()
        } else {
            self.tableView.mj_footer.resetNoMoreData()
        }
    }
  • 导致结果:当所有数据都加载完毕后,重新下拉刷新,然后上拉加载时,由于footer处于nomoredata状态,而无法加载其他页的数据。

  • 分析:
    从pullup代码来看,当上拉加载完所有数据后,将状态更新为了nomoredata状态。
    当重新在该页面进行下拉刷新时,数据只加载page == 1 的数据,但是此时footer.state == nomoredata。
    当要进行上拉加载page == 2 的数据时,由于state == nomoredata,在源码中就直接结束了整个刷新的操作.

  • 源码:
    scrollViewContentOffsetDidChange:

    ~BackFooter:

    // 如果已全部加载,仅设置pullingPercent,然后返回
    if (self.state == MJRefreshStateNoMoreData) {
        self.pullingPercent = pullingPercent;
        return;
    }
    

    ~AutoFooter:

      if (self.state != MJRefreshStateIdle 
          || !self.automaticallyRefresh 
          || self.mj_y == 0) 
        return;
    
  • 解决方案:

      if pullup {
          self.tableView.endFooterRefreshing(noMoreData: noMoreData)
      } else {
          self.tableView.endHeaderRefreshing(noMoreData: noMoreData, count: newcount)
      }
    
    // 给scrollview类进行扩展
    extension UIScrollView {
      
      /// 尾部停止刷新
      ///
      /// - Parameter noMoreData: 下次刷新是否有新的数据
      func endFooterRefreshing(noMoreData: Bool) {
          guard mj_footer != nil else { return }
          if noMoreData {
              mj_footer.endRefreshingWithNoMoreData()
          } else {
              footerEndRefreshing(action: nil)
          }
      }
      
      /// 头部停止刷新
      ///
      /// - Parameter noMoreData: 下次刷新是否有新的数据
      /// - count: 返回的结果个数
      func endHeaderRefreshing(noMoreData: Bool, count: Int) {
          guard mj_header != nil else { return }
          mj_header.endRefreshing()
          updateFooterState(noMoreData: noMoreData, count: count)
      }
      
      /// 根据首次刷新数据, 更新footer状态
      ///
      /// - Parameter noMoreData: 下次刷新是否有新的数据
      /// - count: 返回的结果个数
      private func updateFooterState(noMoreData: Bool, count: Int) {
          guard mj_footer != nil else { return }
          if noMoreData {
              mj_footer.endRefreshingWithNoMoreData()
              if count == 0 {
                  mj_footer.isHidden = true
              }
          } else {
              mj_footer.isHidden = false
              mj_footer.resetNoMoreData()
          }
      }
    }
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 本文转载自J_Knight 的MJRefresh源码解析 MJRefresh是李明杰的作品,到现在已经有9800多...
    Detective41阅读 664评论 0 1
  • 1.前言 MJRefresh 是日常 iOS 开发中使用频率比较高的一款下拉刷新/上拉加载更多的第三方控件,平时似...
    RiverSea阅读 1,408评论 0 10
  • 李明杰老师的代表作之一MJRefresh可以说是棒棒的,很多小伙伴都会在没有什么特殊要求的情况下使用这个框架,简单...
    sunxu_cocoa阅读 529评论 0 1
  • MJRefresh是流行的下拉刷新控件,前段时间为了修复一个BUG,读了它的源码,本文总结一下实现的原理 下拉刷新...
    晚安的你我阅读 456评论 0 0
  • 下拉刷新01-默认 self.tableView.header = [MJRefreshNormalHeader ...
    Lewis海阅读 39,488评论 12 14