MJRefresh源码阅读1——结构梳理
前言
MJRefresh
几乎是我们最常见的开源控件了。很有必要研究研究,但我们为节约时间,只研究这个控件的header
部分,且只研究只有菊花和文字的普通样式的。footer
和header
类似,菊花型的刷新样式和Gif图片型的样式更是同理。
UIScrollView的分类:
给tableView
添加刷新header
:
[_tableView addLegendHeaderWithRefreshingTarget:self refreshingAction:@selector(pullRefresh)];
结束刷新:
[_tableView.header endRefreshing];
MJRefresh
控件是如此友好,开发者使用只需要短短一两行代码。它是直接以UITableView
的实例tableView
对象来操作的,且结束刷新时,我们看到它是以tableView
的header
属性调用endRefreshing
方法的。我们知道UITableView
是没有header
属性的,它这个应该是通过UITableView
分类的方式来动态添加地属性。这么做具有低侵入性的优点,不然也可以通过派生UITableView
的方式来给其添加header
属性,但这样做开发者在有刷新的地方就不能用UITableView
,而一定要用其派生类了,这是不好的。自定义控件,让使用者用起来越简便越好。
MJRefresh
确实是以分类的方式添加刷新header
和footer
的,而且是UIScrollView
的分类,因为该控件对UITableView
和UICollectionView
都支持刷新。
我们来看UIScollView
分类UIScollView+MJRefresh
的源码:
整体来看,在分类的头文件(UIScollView+MJRefresh.h
)中总共有这么些东西:
可以看到所有的属性和方法分为header
和footer
两部分,而且无论是添加header
抑或footer
的方法都提供了好几个接口。就以header
来说添加刷新头就有addLegendHeader
(传统样式刷新头)和addGifHeader
(Gif样式刷新头)两类。而且单就以其中一类样式来看,也分别提供了两组回调方式不同的接口,即block
和SEL
两种方式。而且,在同一回调方式的一组里也有两个方法:一个接口有dateKey
参数,一个没有这个参数。dateKey
参数代表“时间的key”,默认的刷新控件有显示这个页面上次刷新是什么时候,(更正:每个页面上次刷新的时间在MJRefresh
内部维护了一个每个页面最后一次刷新时间的字典,该字典就以传入的dateKey
为key
MJRefresh
中没有以字典来维护,而是将每个界面上次刷新时间直接存入NSUserDefaults
,正是以dateKey
作为key
来区分页面的)。
我们忽略其他代码,只看添加一个以SEL
回调方式的legendHeader
的代码。那么分类的头文件就是这样的:
@interface UIScrollView (MJRefresh)
#pragma mark - 访问下拉刷新控件
/** 下拉刷新控件 */
@property (strong, nonatomic, readonly) MJRefreshHeader *header;
/** 传统的下拉刷新控件 */
@property (nonatomic, readonly) MJRefreshLegendHeader *legendHeader;
...
/**
* 添加一个传统的下拉刷新控件
*
* @param target 进入刷新状态就会自动调用target对象的action方法
* @param action 进入刷新状态就会自动调用target对象的action方法
*/
- (MJRefreshLegendHeader *)addLegendHeaderWithRefreshingTarget:(id)target refreshingAction:(SEL)action;
/**
* 添加一个传统的下拉刷新控件
*
* @param target 进入刷新状态就会自动调用target对象的action方法
* @param action 进入刷新状态就会自动调用target对象的action方法
* @param dateKey 用来记录刷新时间的key
*/
- (MJRefreshLegendHeader *)addLegendHeaderWithRefreshingTarget:(id)target refreshingAction:(SEL)action dateKey:(NSString *)dateKey;
/**
* 移除下拉刷新控件
*/
- (void)removeHeader;
@end
同样的我们也忽略.m
文件中其他代码:
@implementation UIScrollView (MJRefresh)
#pragma mark - 下拉刷新
- (MJRefreshLegendHeader *)addLegendHeaderWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
return [self addLegendHeaderWithRefreshingTarget:target refreshingAction:action dateKey:nil];
}
- (MJRefreshLegendHeader *)addLegendHeaderWithRefreshingTarget:(id)target refreshingAction:(SEL)action dateKey:(NSString *)dateKey
{
MJRefreshLegendHeader *header = [self addLegendHeader];
header.refreshingTarget = target;
header.refreshingAction = action;
header.dateKey = dateKey;
return header;
}
- (MJRefreshLegendHeader *)addLegendHeader
{
MJRefreshLegendHeader *header = [[MJRefreshLegendHeader alloc] init];
self.header = header; // 将header赋值给了scrollView的属性header
return header;
}
- (void)removeHeader
{
self.header = nil;
}
#pragma mark - Property Methods
- (MJRefreshHeader *)header
{
return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}
- (MJRefreshLegendHeader *)legendHeader
{
if ([self.header isKindOfClass:[MJRefreshLegendHeader class]]) {
return (MJRefreshLegendHeader *)self.header;
}
return nil;
}
static char MJRefreshHeaderKey;
- (void)setHeader:(MJRefreshHeader *)header
{
if (header != self.header) {
[self.header removeFromSuperview];
[self willChangeValueForKey:@"header"];
objc_setAssociatedObject(self, &MJRefreshHeaderKey,
header,
OBJC_ASSOCIATION_ASSIGN);
[self didChangeValueForKey:@"header"];
[self addSubview:header];
}
}
#pragma mark - swizzle
+ (void)load
{
Method method1 = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc"));
Method method2 = class_getInstanceMethod([self class], @selector(deallocSwizzle));
method_exchangeImplementations(method1, method2);
}
- (void)deallocSwizzle
{
[self removeFooter];
[self removeHeader];
[self deallocSwizzle];
}
@end
可以看到在addLegendHeaderWithRefreshingTarget: refreshingAction: dateKey:
方法里创建了MJRefreshLegendHeader
类型的header
,并将该方法的target
,action
,dateKey
分别赋给header
里与之对应的属性。
而且在创建header
实例后将其赋给分类的属性header
。我们知道分类中的属性是不会自动生成setter/getter
方法的,要通过运行时(runtime)来实现。这个我们可以在上面的代码中看到,setHeader:
方法里除了通过runtime
来动态生成属性,还手动为其添加了观察(KVC和KVO的使用及原理),最后将header
视图添加在了scrollView
上。
在分类的最后,还以Method Swizzling
的方式动态交换了dealloc
和deallocSwizzle
俩方法的IMP
(方法实现),使得在执行dealloc
方法时实际上跑的是deallocSwizzle
方法的实现,而在deallocSwizzle
方法里移除了header
和footer
。
总结一下,这个分类主要功能是提供了一个添加
header
的很便捷的接口。在接口方法里创建了header
实例,将其赋为分类的属性,并添加在scrollView
上(addSubView:
)。
header的派生类——MJRefreshLegendHeader:
既然上面在分类中创建了MJRefreshLegendHeader
类型的实例,它是一个header
的派生子类,表示传统样式的header
。可以看到在它的头文件中没有暴露给外部任何东西,只能看到它是继承于MJRefreshHeader
类的。
#import "MJRefreshHeader.h"
@interface MJRefreshLegendHeader : MJRefreshHeader
@end
再看它的.m
文件中是怎么实现的:
#import "MJRefreshLegendHeader.h"
#import "MJRefreshConst.h"
#import "UIView+MJExtension.h"
@interface MJRefreshLegendHeader()
@property (nonatomic, weak) UIImageView *arrowImage;
@property (nonatomic, weak) UIActivityIndicatorView *activityView;
@end
@implementation MJRefreshLegendHeader
#pragma mark - 懒加载
- (UIImageView *)arrowImage
{
if (!_arrowImage) {
UIImageView *arrowImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:MJRefreshSrcName(@"arrow.png")]];
[self addSubview:_arrowImage = arrowImage];
}
return _arrowImage;
}
- (UIActivityIndicatorView *)activityView
{
if (!_activityView) {
UIActivityIndicatorView *activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
activityView.bounds = self.arrowImage.bounds;
[self addSubview:_activityView = activityView];
}
return _activityView;
}
#pragma mark - 初始化
- (void)layoutSubviews
{
[super layoutSubviews];
// 箭头
CGFloat arrowX = (self.stateHidden && self.updatedTimeHidden) ? self.mj_w * 0.5 : (self.mj_w * 0.5 - 100);
self.arrowImage.center = CGPointMake(arrowX, self.mj_h * 0.5);
// 指示器
self.activityView.center = self.arrowImage.center;
}
#pragma mark - 公共方法
#pragma mark 设置状态
- (void)setState:(MJRefreshHeaderState)state
{
if (self.state == state) return;
// 旧状态
MJRefreshHeaderState oldState = self.state;
switch (state) {
case MJRefreshHeaderStateIdle: {
if (oldState == MJRefreshHeaderStateRefreshing) {
self.arrowImage.transform = CGAffineTransformIdentity;
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.activityView.alpha = 0.0;
} completion:^(BOOL finished) {
self.arrowImage.alpha = 1.0;
self.activityView.alpha = 1.0;
[self.activityView stopAnimating];
}];
} else {
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowImage.transform = CGAffineTransformIdentity;
}];
}
break;
}
case MJRefreshHeaderStatePulling: {
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowImage.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
}];
break;
}
case MJRefreshHeaderStateRefreshing: {
[self.activityView startAnimating];
self.arrowImage.alpha = 0.0;
break;
}
default:
break;
}
// super里面有回调,应该在最后面调用
[super setState:state];
}
@end
看了实现文件我们可以明白legendHeader
是什么样的了:它在父类原有header
的基础上新添了一个箭头图标和一个菊花视图。在下拉的过程中的不同状态下,箭头和菊花会做出相应的动画显示。这也就是整个MJRefreshLegendHeader
实现文件所做的事。
可以看到,作者重写了arrowImage
和activityView
俩视图的getter
方法来创建其实例,赋给属性,并addSubView:
在MJRefreshLegendHeader
的实例上。
而且作者是在layoutSubviews
这个方法中来设置调整俩视图的坐标位置的。
然后就是最核心的setState:
方法了,它是一个重写的父类的setter
方法,state
是其父类MJRefreshHeader
中的属性。这个方法的功能是当header
在不同状态时,刷新头显示相应的动画。
说起状态,在其父类中定义了一个表示“状态”的枚举。
// 下拉刷新控件的状态
typedef enum {
/** 普通闲置状态 */
MJRefreshHeaderStateIdle = 1,
/** 松开就可以进行刷新的状态 */
MJRefreshHeaderStatePulling,
/** 正在刷新中的状态 */
MJRefreshHeaderStateRefreshing,
/** 即将刷新的状态 */
MJRefreshHeaderStateWillRefresh
} MJRefreshHeaderState;
MJRefreshHeaderStateIdle
,即默认/闲置状态,图示:
MJRefreshHeaderStatePulling
,即已经拉过临界值,松开手便会触发刷新,图示:屏幕快照 2017-01-03 22.36.57.png
MJRefreshHeaderStateRefreshing
,即已松开手,正在刷新,图示:屏幕快照 2017-01-03 23.02.32.png
我们梳理一下下拉刷新过程的逻辑:
我们刚开始往下还没超过临界值时,如图一所示:箭头是向下的,文字为“下拉可以刷新”。对应的代码为第一个case
中的else
部分;
当我们继续往下拉超过临界值时,如图二所示:箭头旋转为向上,文字变为“松开可以刷新”。对应的代码为第二个case
,即MJRefreshHeaderStatePulling
部分;
当我们松开手后,如图三所示:箭头不见了,菊花出现并转动,文字变为“正在刷新数据中...”,并且请留意时间也更新了。对应的代码为第一个case
中if
部分。
整个过程包括了箭头和菊花的显示变化,文字的变化,以及时间的变化。但在该类的代码中只写了箭头和菊花的显示变化,文字和时间的变化是在其父类的setState:
方法中完成的,在该方法的结尾它调用了父类的setState:
方法,这我们都看到了。
该类是header
的派生子类,它只处理了新派生出的功能,即箭头和菊花。而父类处理了文字和时间,那我们也大概能猜测出其父类MJRefreshHeader
的header
是个什么模样:它没有菊花和箭头,但已有文字和时间,且能根据不同状态来变化文字和时间的显示。
header的基类——MJRefreshComponent:
从上面我们已知道派生类MJRefreshLegendHeader
主要是在其父类的基础上添加了箭头和菊花的处理,其父类MJRefreshHeader
已经是个成型的刷新头了。麻雀虽小,五脏俱全。它能处理状态变化的逻辑,虽然没有图标动画显示,但有文本跟随状态而变化。但MJRefreshHeader
还不是header
的尽头,它仍然继承于一个叫MJRefreshComponent
的类,意为“刷新组件”,它定义和实现了刷新的最基本行为。我们来看看它的代码:
MJRefreshComponent.h
文件:
@interface MJRefreshComponent : UIView
{
UIEdgeInsets _scrollViewOriginalInset;
__weak UIScrollView *_scrollView;
}
#pragma mark - 文字处理
/** 文字颜色 */
@property (strong, nonatomic) UIColor *textColor;
/** 字体大小 */
@property (strong, nonatomic) UIFont *font;
#pragma mark - 刷新处理
/** 正在刷新的回调 */
@property (copy, nonatomic) void (^refreshingBlock)();
/** 设置回调对象和回调方法 */
- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action;
@property (weak, nonatomic) id refreshingTarget;
@property (assign, nonatomic) SEL refreshingAction;
/** 进入刷新状态 */
- (void)beginRefreshing;
/** 结束刷新状态 */
- (void)endRefreshing;
/** 是否正在刷新 */
- (BOOL)isRefreshing;
@end
MJRefreshComponent.m
文件:
@interface MJRefreshComponent()
/** 记录scrollView刚开始的inset */
@property (assign, nonatomic) UIEdgeInsets scrollViewOriginalInset;
/** 父控件 */
@property (weak, nonatomic) UIScrollView *scrollView;
@end
@implementation MJRefreshComponent
#pragma mark - 初始化
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// 基本属性
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.backgroundColor = [UIColor clearColor];
// 默认文字颜色和字体大小
self.textColor = MJRefreshLabelTextColor;
self.font = MJRefreshLabelFont;
}
return self;
}
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 旧的父控件
[self.superview removeObserver:self forKeyPath:MJRefreshContentOffset context:nil];
if (newSuperview) { // 新的父控件
[newSuperview addObserver:self forKeyPath:MJRefreshContentOffset options:NSKeyValueObservingOptionNew context:nil];
// 设置宽度
self.mj_w = newSuperview.mj_w;
// 设置位置
self.mj_x = 0;
// 记录UIScrollView
self.scrollView = (UIScrollView *)newSuperview;
// 设置永远支持垂直弹簧效果
self.scrollView.alwaysBounceVertical = YES;
// 记录UIScrollView最开始的contentInset
self.scrollViewOriginalInset = self.scrollView.contentInset;
}
}
#pragma mark - 公共方法
- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action
{
self.refreshingTarget = target;
self.refreshingAction = action;
}
- (void)beginRefreshing
{
}
- (void)endRefreshing
{
}
- (BOOL)isRefreshing {
return NO;
}
@end
可以看到MJRefreshComponent
类作为header
的基类,它只是实现了一个header
最基本最宏观的东西,具体逻辑是没有的。比如只有一些基本属性的设置,一些基本行为的方法,而且方法并未实现,等着让子类去重写实现。而且实现了一个最根本最核心的行为,就是当header
被添加到scrollView
上时,监听scrollView
的contentOffset
属性,这是这个控件的最核心行为,一切状态变化,以及状态变化后引起的UI变化都由此“监听”而生。
结尾
本篇我们首先研究了提供添加header
的接口的分类;然后研究了一个有箭头和菊花显示样式的,header
的派生类;最后研究了其父类的父类MJRefreshComponent
,而跳过了MJRefreshHeader
类。之所以跳过该类,是因为该类是MJRefresh
的核心类,它实现了一个成型的,可以运转的header
,但本篇篇幅已经够长,因此会另起一篇。