MJRefresh这个刷新控件是一款非常好用的框架,我们在使用一个框架的同时,最好能了解下它的实现原理,不管是根据业务要求在原有的基础上修改代码,还是其他的目的,弄明白作者的思路和代码风格,会受益匪浅。
前言
随着开发经验的不断积累,个人的能力也会不断提高。每个人的进步都会有一个过程,这个过程就好比登山。一个框架对我们的意义从开始的简单实用,慢慢的就会过渡到了解,最终让它成为你大脑的一部分。这篇文章不会对代码进行逐行解读,最重要的目的是让朋友们明白 MJRefresh 是怎么做到刷新的,我们还能如何扩展它的功能。
原理部分
我们在 MJRefresh 的 MJRefresh Github 上拿到了下边这张图:
正如这张图片所说的,所有的功能的实现都基于 MJRefreshComponent 这个刷新组件。MJRefreshComponent 有两个分支 MJRefreshHeader(给了下拉刷新的能力) MJRefreshFooter(给了上拉刷新的能力)。
在进行下边的代码分析之前,我们来看看刷新的整个过程(我们以头部刷新为例):
-
tableView.mj_header
这样使用,说明tableview有mj_header
这个属性。然后我们看一下UIScrollView+MJRefresh
这个文件。原理是给这个scrollview添加了一个控件,代码如下 - 我看了下
MJRefreshHeader
的这个文件的头文件,对我们理解原理并没有帮助,由于它是继承MJRefreshComponent
的,所以我们打开MJRefreshComponent
的头文件来看。 -
这几个状态非常关键,是使用整个框架的核心思路。为什么这么说?如果我们在滑动的过程中,状态也会改变,那么必然会调用状态的setter方法,我们就利用这个方法,显示自己想要的效果。
-
prepare
中添加控件,在placeSubviews
中布局,通过scrollViewContentOffsetDidChange:
方法指导contentOffset改变了。然后就能自定义事件了。 - 最终我们在我们最需要的状态上绑定事件就ok了。
MJRefreshComponent
MJRefreshComponent 这个类是最最基础能力的搭建了。除了暴露出业务接口外。值得注意的有两点:
- 上边
UIScrollView+MJRefresh
中提到把mj_header
这个view加到了scrollview上,那么MJRefreshComponent是如何获取scrollview的呢? - 如何监听我们需要的变化?比如说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];
}
}
}
演示案例
我们就简单演示 转转 的下拉加载效果。由于代码改动比较小,在这里下载:转转下拉效果
总结
有时间写一个类似知乎刷新那样的效果。