MJRefresh源码阅读
来源:互联网 发布:激光雷达避障算法 编辑:程序博客网 时间:2024/06/16 13:27
本文对应的MJRefresh版本是3.1.12
使用
MJRefresh的使用很简单,引入MJRefresh.h头文件,然后创建header或者footer赋值给tableView或者collectionView的mj_header或者mj_footer属性。设置好block或者target-action的回调就可以了。
接口也非常简单,如果没有特殊要求,提供的默认的样式已经很好了。同时MJRefresh内置的几个header和footer可以定制不同的样式,如果还不能满足需求,还可以自己写header和footer。
作者GitHub上的示例代码:
// 下拉刷新 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_footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{ // 模拟延迟加载数据,因此2秒后才调用(真实开发中,可以移除这段gcd代码) dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 结束刷新 [tableView.mj_footer endRefreshing]; }); }];
原理
- 通过OC的runtime,在Category中给UIScrollView增加属性,mj_header和mj_footer。
- mj_header和mj_footer使用KVO观察UIScrollView的contentOffset属性,根据属性的值判断上拉和下拉的状态。
- mj_header和mj_footer的子类负责具体的显示样式和对上拉下拉状态的响应。
源码阅读
主要类图:
从引入的头文件开始,MJRefresh.h中又包含了好几个头文件,用途分为3种类型:
第一种是辅助类型的文件:
#import "UIScrollView+MJExtension.h"#import "UIView+MJExtension.h”
MJExtension的分类都是View上的辅助方法,为了写代码好看用的。比如UIScrollView(MJExtension)中的mj_offsetX属性,getter和setter方法是这样的:
- (CGFloat)mj_offsetX{ return self.contentOffset.x;}- (void)setMj_offsetY:(CGFloat)mj_offsetY{ CGPoint offset = self.contentOffset; offset.y = mj_offsetY; self.contentOffset = offset;}
所以这部分代码可以略过不看。
第二种是MJRefresh提供的header和footer
#import "MJRefreshNormalHeader.h"#import "MJRefreshGifHeader.h"#import "MJRefreshBackNormalFooter.h"...
第三种是UIScrollView的MJRefresh分类。
#import "UIScrollView+MJRefresh.h”
这个是阅读代码的入口。
UIScrollView的mj_header和mj_footer属性
在UIScrollView+MJRefresh.h中,给UIScrollView添加了两个属性:mj_header和mj_footer。使用runtime添加属性需要自己写getter和setter方法,在UIScrollView+MJRefresh.m文件中可以看到相关代码:
static const char MJRefreshHeaderKey = '\0';- (void)setMj_header:(MJRefreshHeader *)mj_header{ if (mj_header != self.mj_header) { // 删除旧的,添加新的 [self.mj_header removeFromSuperview]; [self insertSubview:mj_header atIndex:0]; // 存储新的 [self willChangeValueForKey:@"mj_header"]; // KVO objc_setAssociatedObject(self, &MJRefreshHeaderKey, mj_header, OBJC_ASSOCIATION_ASSIGN); [self didChangeValueForKey:@"mj_header"]; // KVO }}- (MJRefreshHeader *)mj_header{ return objc_getAssociatedObject(self, &MJRefreshHeaderKey);}
MJRefreshComponent。
MJRefresh对上拉和下拉的事件响应有两种方式,一种是回调block,另外一种是target-action。
/** 创建header */+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock;/** 创建header */+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action;
在什么情况下回执行回调呢?,从上边的类图可以看到,MJRefreshComponent是所有footer和header的基类,由它来处理UIScrollView和下来上拉刷新的所有状态,在MJRefreshComponent.h文件中定义了下拉上拉的状态:
/** 刷新控件的状态 */typedef NS_ENUM(NSInteger, MJRefreshState) { /** 普通闲置状态 */ MJRefreshStateIdle = 1, /** 松开就可以进行刷新的状态 */ MJRefreshStatePulling, /** 正在刷新中的状态 */ MJRefreshStateRefreshing, /** 即将刷新的状态 */ MJRefreshStateWillRefresh, /** 所有数据加载完毕,没有更多的数据了 */ MJRefreshStateNoMoreData};
MJRefreshComponent在willMoveToSuperview中调用了addObserver函数对UIScrollView进行了监控:
- (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];}
观察了UIScrollView的contentOffset,contentSize,pan手势3个能影响下拉上拉状态的属性。
在对观察的响应中,MJRefreshComponent并没有真正的处理这些变化,而是调用了3个抽象方法,具体的处理交给了子类去做。
- (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]; }}- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
这3个方法并没有实现,是抽象方法。(在整个代码中看见只有autofooter类型的footer使用了scrollViewPanStateDidChange,并且因为响应了这个事件,还引入了bug,automaticallyRefresh=NO不起作用了,所以我使用的时候将这个监控去掉了)
MJRefreshComponent并没有对状态做判断,除了很确定的情况:在beginRefreshing和endRefreshing中设置了refreshing和idle状态。
MJRefreshComponent也并没有决定,在什么状态下调用刷新的回调,只留下一个executeRefreshingCallback方法供子类使用。
MJRefreshComponent并没有去创建或者布局界面,只是留了2个接口:prepare, placeSubviews
prepare创建过程中的一个钩子,在这里可以创建subview添加到控件中,
placeSubviews是layoutSubviews过程中的一个钩子,在这里可以布局subview。
MJRefreshHeader
MJRefreshHeader主要是处理了状态,即重写了这个方法:
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
上边提到scrollViewContentOffsetDidChange, scrollViewContentSizeDidChange和scrollViewPanStateDidChange这3个方法决定着刷新控件的状态,和header的状态有关的就只是scrollViewContentOffsetDidChange。这个方法就是根据contentOffset的大小来决定当前的状态的。
到这层,header的状态跳转就能完全确定下来了,所以MJRefreshHeader的子类以及子类的子类都不需要处理下拉状态相关的事情了。
由于状态能确定了,所以根据状态变化的控件UI部分也能确定了,所以在setState(属性state的setter方法)中处理了一些和state相关的事宜,具体说来就是:
- 保存了刷新时间
- 恢复scrollView的inset
- 设置整个控件的alpha值(可选)
- 调用refreshing的回调(在基类MJRefreshComponent中定义的executeRefreshingCallback)
在MJRefreshHeader的头文件中有两个class方法:
/** 创建header */+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock;/** 创建header */+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action;
这两个方法可以实例化出MJRefreshHeader了,说明MJRefreshHeader已经是可实例化使用的header了,试一下确实可以使用下拉刷新,但是没有显示header的界面,因为MJRefreshHeader只处理了状态,但是没有添加任何subView显示。作者在下一层中才给出一个真正完备功能的header。
MJRefreshStateHeader
如果创建了一个MJRefreshStateHeader赋值给mj_header。可以看到下拉刷新显示当前状态文字,显示刷新时间显示文字。
由于有了MJRefreshComponent和MJRefreshHeader提供的基础设施,实现上述功能很简单:MJRefreshStateHeader添加了stateLabel和lastUpdateTimeLabel属性,并在重写的prepare方法中创建了它们。在placeSubviews方法中布局了它们。在setState中设置它们的文字。其他的事情不需要了
MJRefreshNormalHeader和MJRefreshGifHeader
这两个类是继承自MJRefreshStateHeader的,MJRefreshNormalHeader在stateLabel之前增加了一个表示状态的箭头图片和一个loading的转菊花。还是那3件事:
- 在prepare中添加subview
- 在placeSubviews中布局subview
- 在setState中设置subView的状态(注意调用super,写在了宏中)
注:其实按照作者思路,应该在prepare中addSubview,但是arrowView的懒加载中调用了addSubview,而arrowView的getter是在placeSubviews中调用的,所以实际是在placeSubviews中addSubView的。
MJRefreshGifHeader是使用一个UIImageView来代替MJRefreshNormalHeader中的箭头和菊花,具体做法就是:
- 增加一个stateImage,UIImageView类型(prepare中添加,placeSubviews中布局)
- 给stateImage不同的状态设置不同的图片组
- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state
在setState中根据不同的state播放不同的图片组。
重写MJRefreshComponent的setPullingPercent方法。能根据不同的下拉程度显示动态的效果。
Header的所有类就这些了,如果需求能被上述类Cover到,直接使用即可,一些可定制话的属性并没有介绍,比如可以选则隐藏stateLabel,或者改变stateLabel的文字颜色等。可以参考官网的Example。
如果上述类不能满足要求,那么应该从相应的层入手,比如官网的Example中有一个下来的吃包子的状态动画,就是继承自MJRefreshGifHeader,如果你的Header和状态无关,也可以继承自MJRefreshHeader。但是应该没有理由从MJRefreshComponent中继承,因为MJRefreshHeader处理了状态,而这些状态是确定的。
footer
footer比header要复杂些。需求一般有:
- 涉及到没有更多数据的显示,并在在上拉之后还要恢复刷加载更多的状态
- 加载更多数据可以是到底部自动触发,也可以是有一个人为的操作才触发
- 触发可以是上拉或者点击按钮
- 数据不满一屏幕的时候footer是显示出来还是隐藏。
因为这些需求,MJRefreshFooter也不能确定下来footer的各个状态,因此在MJRefreshFooter中,仅仅做了几个简单属性的设置:高度,是否隐藏。对状态的控制就是在调用公开接口endRefreshingWithNoMoreData中将状态设置成为MJRefreshStateNoMoreData,在resetNoMoreData中设置状态为MJRefreshStateIdle。这些都是确定的行为。
根据上边的需求,作者将footer分为两种类型:MJRefreshAutoFooter和MJRefreshBackFooter。AutoFooter是只要scrollview滚动到底部控件都显示出来之后,就自动触发刷新,这个特性可以被禁止(将属性automaticallyRefresh设置为NO)。BackFooter是隐藏在屏幕之外的,要上拉才能看到,上拉到一定程度触发数据加载。上拉不到位就弹回屏幕下边。
可以看出backfooter的行为和header有些类似,所以对scrollViewContentOffsetDidChange的响应和header也是类似的,autofooter就简单了许多,只要区分现有的数据显示比一个屏幕长还是在一个屏幕之内:
- (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]; } }else if(_scrollView.mj_offsetY > -_scrollView.mj_insetT + self.mj_h){ // 防止手松开时连续调用 CGPoint old = [change[@"old"] CGPointValue]; CGPoint new = [change[@"new"] CGPointValue]; if (new.y <= old.y) return; // 当底部刷新控件完全出现时,才刷新 [self beginRefreshing]; }}
else if部分的代码是我加的,意思就是当数据不满一个屏幕的时候,如果上拉的高度大于了控件本身的高度,就触发刷新。加上这段代码之后就不需要响应scrollViewPanStateDidChange这个事件了。目前使用作者的test case测试还没有发现问题。
和header不同,footer的y坐标是要变化的,所以autofooter和backfooter都实现了- (void)scrollViewContentSizeDidChange:(NSDictionary *)change 方法,当scrollView的contentSize变化之后,重新调整y坐标。
MJRefreshAutoFooter和MJRefreshBackFooter在继承体系中的地位和MJRefreshHeader是一样的,因此,如果需要重写Footer,先确定是那种类型的Footer然后继承MJRefreshAutoFooter或者MJRefreshBackFooter就可以了。
如果没有特殊需求使用MJRefreshAutoNormalFooter或者MJRefreshBackNormalFooter就可以了。如果有和状态相关的动画需求使用MJRefreshAutoGifFooter和MJRefreshBackGifFooter即可。这些类的代码和Header几乎一样。
- MJRefresh源码阅读
- iOS MJRefresh源码研读
- iOS: MJRefresh源码分析
- iOS: MJRefresh源码分析
- MJRefresh源码解析
- MJRefresh 源码详细解析
- iOS: MJRefresh源码分析
- MJRefresh
- MJrefresh
- MJRefresh
- MJRefresh
- MJRefresh
- MJRefresh
- MJRefresh
- 阅读源码
- 阅读源码
- 阅读源码
- 源码阅读
- 连续子数组的最大和(剑指Offer)
- R中的包
- IntelliJ IDEA 2017激活永久破解方法
- Requests使用入门
- 南阳oj[116]士兵杀敌(二)
- MJRefresh源码阅读
- HDU-1698-Just a Hook (区间修改【已知修改后的值】)
- Java多线程知识小抄集(三)
- 构造方法相关(与类名相同的方法)
- 如何在python项目中更加优美的自定义常量
- 网易笔试题总结
- react-app
- vue组件学习6(props传参)
- Mac下使用node进程管理工具supervisor