前言
MJRefresh
幾乎是我們最常見的開源控件了芹血。很有必要研究研究,但我們?yōu)楣?jié)約時間,只研究這個控件的header
部分啰扛,且只研究只有菊花和文字的普通樣式的。footer
和header
類似嗡贺,菊花型的刷新樣式和Gif圖片型的樣式更是同理隐解。
UIScrollView的分類:
給tableView
添加刷新header
:
[_tableView addLegendHeaderWithRefreshingTarget:self refreshingAction:@selector(pullRefresh)];
結(jié)束刷新:
[_tableView.header endRefreshing];
MJRefresh
控件是如此友好,開發(fā)者使用只需要短短一兩行代碼诫睬。它是直接以UITableView
的實(shí)例tableView
對象來操作的煞茫,且結(jié)束刷新時,我們看到它是以tableView
的header
屬性調(diào)用endRefreshing
方法的岩臣。我們知道UITableView
是沒有header
屬性的溜嗜,它這個應(yīng)該是通過UITableView
分類的方式來動態(tài)添加地屬性。這么做具有低侵入性的優(yōu)點(diǎn)架谎,不然也可以通過派生UITableView
的方式來給其添加header
屬性炸宵,但這樣做開發(fā)者在有刷新的地方就不能用UITableView
,而一定要用其派生類了谷扣,這是不好的土全。自定義控件,讓使用者用起來越簡便越好会涎。
MJRefresh
確實(shí)是以分類的方式添加刷新header
和footer
的裹匙,而且是UIScrollView
的分類,因?yàn)樵摽丶?code>UITableView和UICollectionView
都支持刷新末秃。
我們來看UIScollView
分類UIScollView+MJRefresh
的源碼:
整體來看概页,在分類的頭文件(UIScollView+MJRefresh.h
)中總共有這么些東西:
可以看到所有的屬性和方法分為header
和footer
兩部分,而且無論是添加header
抑或footer
的方法都提供了好幾個接口练慕。就以header
來說添加刷新頭就有addLegendHeader
(傳統(tǒng)樣式刷新頭)和addGifHeader
(Gif樣式刷新頭)兩類惰匙。而且單就以其中一類樣式來看,也分別提供了兩組回調(diào)方式不同的接口铃将,即block
和SEL
兩種方式项鬼。而且,在同一回調(diào)方式的一組里也有兩個方法:一個接口有dateKey
參數(shù)劲阎,一個沒有這個參數(shù)绘盟。dateKey
參數(shù)代表“時間的key”,默認(rèn)的刷新控件有顯示這個頁面上次刷新是什么時候,(更正:每個頁面上次刷新的時間在MJRefresh
內(nèi)部維護(hù)了一個每個頁面最后一次刷新時間的字典龄毡,該字典就以傳入的dateKey
為key
MJRefresh
中沒有以字典來維護(hù)吠卷,而是將每個界面上次刷新時間直接存入NSUserDefaults
,正是以dateKey
作為key
來區(qū)分頁面的)沦零。
我們忽略其他代碼撤嫩,只看添加一個以SEL
回調(diào)方式的legendHeader
的代碼。那么分類的頭文件就是這樣的:
@interface UIScrollView (MJRefresh)
#pragma mark - 訪問下拉刷新控件
/** 下拉刷新控件 */
@property (strong, nonatomic, readonly) MJRefreshHeader *header;
/** 傳統(tǒng)的下拉刷新控件 */
@property (nonatomic, readonly) MJRefreshLegendHeader *legendHeader;
...
/**
* 添加一個傳統(tǒng)的下拉刷新控件
*
* @param target 進(jìn)入刷新狀態(tài)就會自動調(diào)用target對象的action方法
* @param action 進(jìn)入刷新狀態(tài)就會自動調(diào)用target對象的action方法
*/
- (MJRefreshLegendHeader *)addLegendHeaderWithRefreshingTarget:(id)target refreshingAction:(SEL)action;
/**
* 添加一個傳統(tǒng)的下拉刷新控件
*
* @param target 進(jìn)入刷新狀態(tài)就會自動調(diào)用target對象的action方法
* @param action 進(jìn)入刷新狀態(tài)就會自動調(diào)用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:
方法里創(chuàng)建了MJRefreshLegendHeader
類型的header
蠢终,并將該方法的target
,action
,dateKey
分別賦給header
里與之對應(yīng)的屬性序攘。
而且在創(chuàng)建header
實(shí)例后將其賦給分類的屬性header
。我們知道分類中的屬性是不會自動生成setter/getter
方法的寻拂,要通過運(yùn)行時(runtime)來實(shí)現(xiàn)程奠。這個我們可以在上面的代碼中看到,setHeader:
方法里除了通過runtime
來動態(tài)生成屬性祭钉,還手動為其添加了觀察(KVC和KVO的使用及原理)瞄沙,最后將header
視圖添加在了scrollView
上。
在分類的最后慌核,還以Method Swizzling
的方式動態(tài)交換了dealloc
和deallocSwizzle
倆方法的IMP
(方法實(shí)現(xiàn))距境,使得在執(zhí)行dealloc
方法時實(shí)際上跑的是deallocSwizzle
方法的實(shí)現(xiàn),而在deallocSwizzle
方法里移除了header
和footer
垮卓。
總結(jié)一下垫桂,這個分類主要功能是提供了一個添加
header
的很便捷的接口。在接口方法里創(chuàng)建了header
實(shí)例粟按,將其賦為分類的屬性诬滩,并添加在scrollView
上(addSubView:
)。
header的派生類——MJRefreshLegendHeader:
既然上面在分類中創(chuàng)建了MJRefreshLegendHeader
類型的實(shí)例灭将,它是一個header
的派生子類疼鸟,表示傳統(tǒng)樣式的header
∶硎铮可以看到在它的頭文件中沒有暴露給外部任何東西空镜,只能看到它是繼承于MJRefreshHeader
類的。
#import "MJRefreshHeader.h"
@interface MJRefreshLegendHeader : MJRefreshHeader
@end
再看它的.m
文件中是怎么實(shí)現(xiàn)的:
#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 設(shè)置狀態(tài)
- (void)setState:(MJRefreshHeaderState)state
{
if (self.state == state) return;
// 舊狀態(tài)
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里面有回調(diào)捌朴,應(yīng)該在最后面調(diào)用
[super setState:state];
}
@end
看了實(shí)現(xiàn)文件我們可以明白legendHeader
是什么樣的了:它在父類原有header
的基礎(chǔ)上新添了一個箭頭圖標(biāo)和一個菊花視圖吴攒。在下拉的過程中的不同狀態(tài)下,箭頭和菊花會做出相應(yīng)的動畫顯示男旗。這也就是整個MJRefreshLegendHeader
實(shí)現(xiàn)文件所做的事舶斧。
可以看到欣鳖,作者重寫了arrowImage
和activityView
倆視圖的getter
方法來創(chuàng)建其實(shí)例察皇,賦給屬性,并addSubView:
在MJRefreshLegendHeader
的實(shí)例上。
而且作者是在layoutSubviews
這個方法中來設(shè)置調(diào)整倆視圖的坐標(biāo)位置的什荣。
然后就是最核心的setState:
方法了矾缓,它是一個重寫的父類的setter
方法,state
是其父類MJRefreshHeader
中的屬性稻爬。這個方法的功能是當(dāng)header
在不同狀態(tài)時嗜闻,刷新頭顯示相應(yīng)的動畫。
說起狀態(tài)桅锄,在其父類中定義了一個表示“狀態(tài)”的枚舉琉雳。
// 下拉刷新控件的狀態(tài)
typedef enum {
/** 普通閑置狀態(tài) */
MJRefreshHeaderStateIdle = 1,
/** 松開就可以進(jìn)行刷新的狀態(tài) */
MJRefreshHeaderStatePulling,
/** 正在刷新中的狀態(tài) */
MJRefreshHeaderStateRefreshing,
/** 即將刷新的狀態(tài) */
MJRefreshHeaderStateWillRefresh
} MJRefreshHeaderState;
MJRefreshHeaderStateIdle
,即默認(rèn)/閑置狀態(tài)友瘤,圖示:
MJRefreshHeaderStatePulling
翠肘,即已經(jīng)拉過臨界值,松開手便會觸發(fā)刷新辫秧,圖示:MJRefreshHeaderStateRefreshing
束倍,即已松開手,正在刷新盟戏,圖示:我們梳理一下下拉刷新過程的邏輯:
我們剛開始往下還沒超過臨界值時绪妹,如圖一所示:箭頭是向下的,文字為“下拉可以刷新”柿究。對應(yīng)的代碼為第一個case
中的else
部分邮旷;
當(dāng)我們繼續(xù)往下拉超過臨界值時,如圖二所示:箭頭旋轉(zhuǎn)為向上蝇摸,文字變?yōu)椤八砷_可以刷新”廊移。對應(yīng)的代碼為第二個case
,即MJRefreshHeaderStatePulling
部分探入;
當(dāng)我們松開手后狡孔,如圖三所示:箭頭不見了,菊花出現(xiàn)并轉(zhuǎn)動蜂嗽,文字變?yōu)椤罢谒⑿聰?shù)據(jù)中...”苗膝,并且請留意時間也更新了。對應(yīng)的代碼為第一個case
中if
部分植旧。
整個過程包括了箭頭和菊花的顯示變化辱揭,文字的變化,以及時間的變化病附。但在該類的代碼中只寫了箭頭和菊花的顯示變化问窃,文字和時間的變化是在其父類的setState:
方法中完成的,在該方法的結(jié)尾它調(diào)用了父類的setState:
方法完沪,這我們都看到了域庇。
該類是header
的派生子類嵌戈,它只處理了新派生出的功能,即箭頭和菊花听皿。而父類處理了文字和時間熟呛,那我們也大概能猜測出其父類MJRefreshHeader
的header
是個什么模樣:它沒有菊花和箭頭,但已有文字和時間尉姨,且能根據(jù)不同狀態(tài)來變化文字和時間的顯示庵朝。
header的基類——MJRefreshComponent:
從上面我們已知道派生類MJRefreshLegendHeader
主要是在其父類的基礎(chǔ)上添加了箭頭和菊花的處理,其父類MJRefreshHeader
已經(jīng)是個成型的刷新頭了又厉。麻雀雖小九府,五臟俱全。它能處理狀態(tài)變化的邏輯,雖然沒有圖標(biāo)動畫顯示,但有文本跟隨狀態(tài)而變化出皇。但MJRefreshHeader
還不是header
的盡頭,它仍然繼承于一個叫MJRefreshComponent
的類勾怒,意為“刷新組件”,它定義和實(shí)現(xiàn)了刷新的最基本行為声旺。我們來看看它的代碼:
MJRefreshComponent.h
文件:
@interface MJRefreshComponent : UIView
{
UIEdgeInsets _scrollViewOriginalInset;
__weak UIScrollView *_scrollView;
}
#pragma mark - 文字處理
/** 文字顏色 */
@property (strong, nonatomic) UIColor *textColor;
/** 字體大小 */
@property (strong, nonatomic) UIFont *font;
#pragma mark - 刷新處理
/** 正在刷新的回調(diào) */
@property (copy, nonatomic) void (^refreshingBlock)();
/** 設(shè)置回調(diào)對象和回調(diào)方法 */
- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action;
@property (weak, nonatomic) id refreshingTarget;
@property (assign, nonatomic) SEL refreshingAction;
/** 進(jìn)入刷新狀態(tài) */
- (void)beginRefreshing;
/** 結(jié)束刷新狀態(tài) */
- (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];
// 默認(rèn)文字顏色和字體大小
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];
// 設(shè)置寬度
self.mj_w = newSuperview.mj_w;
// 設(shè)置位置
self.mj_x = 0;
// 記錄UIScrollView
self.scrollView = (UIScrollView *)newSuperview;
// 設(shè)置永遠(yuǎn)支持垂直彈簧效果
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
的基類笔链,它只是實(shí)現(xiàn)了一個header
最基本最宏觀的東西,具體邏輯是沒有的腮猖。比如只有一些基本屬性的設(shè)置鉴扫,一些基本行為的方法,而且方法并未實(shí)現(xiàn)澈缺,等著讓子類去重寫實(shí)現(xiàn)坪创。而且實(shí)現(xiàn)了一個最根本最核心的行為,就是當(dāng)header
被添加到scrollView
上時姐赡,監(jiān)聽scrollView
的contentOffset
屬性莱预,這是這個控件的最核心行為,一切狀態(tài)變化项滑,以及狀態(tài)變化后引起的UI變化都由此“監(jiān)聽”而生依沮。
結(jié)尾
本篇我們首先研究了提供添加header
的接口的分類;然后研究了一個有箭頭和菊花顯示樣式的枪狂,header
的派生類危喉;最后研究了其父類的父類MJRefreshComponent
,而跳過了MJRefreshHeader
類州疾。之所以跳過該類辜限,是因?yàn)樵擃愂?code>MJRefresh的核心類,它實(shí)現(xiàn)了一個成型的严蓖,可以運(yùn)轉(zhuǎn)的header
薄嫡,但本篇篇幅已經(jīng)夠長氧急,因此會另起一篇。