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)中总共有这么些东西:
屏幕快照 2017-01-03 下午5.31.33.png
可以看到所有的属性和方法分为header和footer两部分,而且无论是添加header抑或footer的方法都提供了好几个接口。就以header来说添加刷新头就有addLegendHeader(传统样式刷新头)和addGifHeader(Gif样式刷新头)两类。而且单就以其中一类样式来看,也分别提供了两组回调方式不同的接口,即block和SEL两种方式。而且,在同一回调方式的一组里也有两个方法:一个接口有dateKey参数,一个没有这个参数。dateKey参数代表“时间的key”,默认的刷新控件有显示这个页面上次刷新是什么时候,(更正:每个页面上次刷新的时间在MJRefresh内部维护了一个每个页面最后一次刷新时间的字典,该字典就以传入的dateKey为keyMJRefresh中没有以字典来维护,而是将每个界面上次刷新时间直接存入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,即默认/闲置状态,图示:
屏幕快照 2017-01-03 22.36.31.png
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,但本篇篇幅已经够长,因此会另起一篇。