背景
写这篇文章主要有两个目的:
- 虽然refresh的源码已经有很多小伙伴分析过了。但是其应用不能说不够广阔。所以,抽空重新梳理了一下该源码,然后记录一下自己的心得。方便之后重新阅读,然后对比更新。
- 另一个问题是发现一个阅读源码的现象:比如源码有100个文件,当阅读了10个文件,就说我阅读过某某某框架的源码,然后看到源码中某某方法和另一方法实现一致,然后就直接调用同一个方法。但是此时就很容易出现问题。有时和你理论的时候,依旧意识不到问题所在,继续翻开源码说实现是一致的。可源码整体看一下,就能发现遇到的问题。最后一种情况将会分析这遇到的问题。
本文从以下几个方面进行记录:
- 继承层次
- 整体分析
- 各类的职责
- 下拉刷新
- 主动调用代码
- UI进行交互
- 上拉刷新
- 常见的错误❌
一、基本类继承层次关系
// 为了方便表示,使用~代替
~ = MJRefresh
~Component: UIView
~Header
~StateHeader
~NormalHeader
~GifHeader
~Footer
~AutoFooter
~AutoStateFooter
~AutoNormalFooter
~AutoGifFooter
~BackFooter
~BackStateFooter
~BackNormalFooter
~BackGifFooter
二、整体分析:
- MJRefreshComponet是继承自UIView。其他所有的类都是其子类。
- mj_header和mj_footer是添加在ScrollView上分类的属性,其类型是Header/Footer类型,并且其添加到了scrollview的view容器内。因此在赋值后,才会出现在头部/尾部。
通篇控制的方式,是通过子类重写其基类的MJRefreshState类型的state属性,重写其set方法进行更改当前刷新的状态。从而控制头部和尾部的展示内容(是否隐藏,内边距,偏移量等)。最终汇聚成看到的展示样貌。
三、各类的职责
要看清各个部件是如何进行逻辑的组织及代码的编写,得分清各个类的职责是什么,负责哪一模块,然后才能更好地理通整个源码的思想。
- ~State模块主要处理状态:时间和不同状态的title
- ~Normal模块主要处理:箭头的方向,是否显示及loadingView的是否显示。
- ~Component基模块:主要暴露相应的接口,block供给子类进行组装及监听等的处理。
- ~Header 、~Footer模块:初始化的操作、设置frame的高度、scrollview的布局操作(inset, offset等)。
四、Header
下拉刷新刷新的方式有两种:一种是首次进入页面的时候,就主动触发代码刷新的操作,使其开始下拉刷新。另一种是手动下拉scrollview,进行的一系列UI操作及其给出的反馈。
主动调用代码进行的刷新操作
调用流程:
-
-[MJRefreshHeader beginRefreshing]
- setState:(refreshing)
- 展示刷新样式(增加滚动区域,更新offest值为header的高度)
- 执行block内部的刷新请求(executeRefreshingCallback)。
- 当3中的请求回调操作执行完之后,当外部主动调用- endRefreshing时进行还原操作。
-
-[MJRefreshHeader endRefreshing]
- setState:(idle)
- 恢复刷新样式,头部消失(更改inset为初始值)
- 如果endrefreshing中有回调时就执行,没有就结束整个流程。
与UI进行交互出现的刷新操作
主要有通过下拉或者上滑出现的UI变动及刷新的逻辑等。后半部分是一致的。主要分析前半部分与UI进行交互,然后进行更新头部的动画的过程。
以下拉刷新为例:(其中h为header的高度)
当下拉scrollview时,触发其delegate方法:scrollViewContentOffsetDidChange:方法。
-
当drag == true 时,state状态一直在变化;
下拉到临界值:
(state == idle && offsetY < -h)时 --> state = pulling。此时状态文字要发生变化,箭头也要进行翻转。根据各类的职责去相应的类中更改即可。上拉到临界值:
state == pulling && offsetY >= -h时,恢复state = idle。同时恢复箭头,文字等的状态。
当drag == false && state == pulling,即在拉过临界值,并且松手了,那么此之后执行的逻辑,就是下拉刷新过程。
五、Footer
比Header包含的状态稍微多一点,但是和header的逻辑是类似的。
Footer分为Auto和Back两种状态
联系:与header的比较,之前处理
~Header
的逻辑现在迁移到了AutoFooter和BackFooter。区别:
~BackFooter
的刷新是通过手动拖拽,并且在tableview的尾部最后进行展示的。而~AutoFooter的刷新操作,当上滑到最后一个cell时,将会自动进行刷新的操作。
对于footer通常不会去主动调用- beginRefresing该方法。他在设置好相应的初始位置后,就开始进行手动与UI
进行交互的上拉刷新操作:
~BackFooter:
- footer的位置在屏幕的下方(无论是contentsize.height是否大于screensize.height).
- 之后的操作就不再赘述,和下拉刷新的流程真的一模一样。(可以通过源码进行对照查看)
~AutoFooter:
- footer.y = contentsize.height + △ (其中△是一些inset信息).
- 之后的刷新也和下拉是一致的内容.
六、常见的错误❌:
问题出现点:在上拉刷新的时候,根据有没有更多数据,来更改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() } } }