項(xiàng)目中經(jīng)常用到下拉刷新的功能,平時(shí)用的最多的是MJRefresh,這個(gè)第三方庫提供了簡單易用的API,方便使用,下面我們來分析一下源碼。
一、常數(shù)類

常量類比較簡單,其中定義了宏,高度、提示語句等常量,以及KVO用到的屬性名。
二、Category

XX+MJExtension中定義獲取/設(shè)置視圖的x、y、w、h、offset、inset等屬性的方法,方便使用。(PS:之前有用宏來定義過,也能達(dá)到簡化代碼的目的,但是確實(shí)沒有用Category方便,尤其當(dāng)宏定義有多行代碼的時(shí)候);
UIScollView+MJRefresh為ScrollView添加了一些方法,我們來逐個(gè)分析。
#pragma mark - header
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];
// 存儲(chǔ)新的
[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);
}
這段代碼利用的runtime的特性,將mj_header綁定到MJRefreshHeaderKey的地址上,從而實(shí)現(xiàn)向Category中添加屬性的效果。在Category中可以使用property,編譯通過,但是運(yùn)行無效。但是在XX+MJExtension中作者也使用到了property,為什么又可以呢?我們仔細(xì)觀察一下代碼:

可以發(fā)現(xiàn)setMj_x方法并不是對mj_x進(jìn)行賦值,mj_x方法返回的也并不是mj_x的值,如果是return self.mj_x,運(yùn)行時(shí)同樣會(huì)報(bào)錯(cuò)。
繼續(xù)往下看:
- (NSInteger)mj_totalDataCount
{
NSInteger totalCount = 0;
if ([self isKindOfClass:[UITableView class]]) {
UITableView *tableView = (UITableView *)self;
for (NSInteger section = 0; section<tableView.numberOfSections; section++) {
totalCount += [tableView numberOfRowsInSection:section];
}
} else if ([self isKindOfClass:[UICollectionView class]]) {
UICollectionView *collectionView = (UICollectionView *)self;
for (NSInteger section = 0; section<collectionView.numberOfSections; section++) {
totalCount += [collectionView numberOfItemsInSection:section];
}
}
return totalCount;
}
這段代碼獲取了tableView或collectionView的元素個(gè)數(shù)。
繼續(xù):
static const char MJRefreshReloadDataBlockKey = '\0';
- (void)setMj_reloadDataBlock:(void (^)(NSInteger))mj_reloadDataBlock
{
[self willChangeValueForKey:@"mj_reloadDataBlock"]; // KVO
objc_setAssociatedObject(self, &MJRefreshReloadDataBlockKey, mj_reloadDataBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
[self didChangeValueForKey:@"mj_reloadDataBlock"]; // KVO
}
- (void (^)(NSInteger))mj_reloadDataBlock
{
return objc_getAssociatedObject(self, &MJRefreshReloadDataBlockKey);
}
- (void)executeReloadDataBlock
{
!self.mj_reloadDataBlock ? : self.mj_reloadDataBlock(self.mj_totalDataCount);
}
@end
給UIScrollView添加了block對象,并確在設(shè)置的時(shí)候手動(dòng)出發(fā)KVO。
Category的最后一部分代碼:
@implementation UITableView (MJRefresh)
+ (void)load
{
[self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
}
- (void)mj_reloadData
{
[self mj_reloadData];
[self executeReloadDataBlock];
}
@end
+ (void)exchangeInstanceMethod1:(SEL)method1 method2:(SEL)method2
{
method_exchangeImplementations(class_getInstanceMethod(self, method1), class_getInstanceMethod(self, method2));
}
+ (void)exchangeClassMethod1:(SEL)method1 method2:(SEL)method2
{
method_exchangeImplementations(class_getClassMethod(self, method1), class_getClassMethod(self, method2));
}
這里用到了Method Swizzling技術(shù),簡而言之就是利用runtime的特性將兩個(gè)類方法或?qū)嵗椒ń粨Q,在這里,作者實(shí)現(xiàn)了每次執(zhí)行完UIScrollView自己的方法reloadData后執(zhí)行[self executeReloadDataBlock]。
三、MJ類
這里直接借用github上的原圖,下圖表示的是MJ各個(gè)類之間的繼承關(guān)系。

我們已Header為例,分析一下每一個(gè)類都做了哪些事情,首先是MJRefreshComponent。
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// 準(zhǔn)備工作
[self prepare];
// 默認(rèn)是普通狀態(tài)
self.state = MJRefreshStateIdle;
}
return self;
}
- (void)prepare
{
// 基本屬性
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.backgroundColor = [UIColor clearColor];
}
設(shè)置基本屬性,設(shè)置刷新控件的狀態(tài),初始化為Idle,其他狀態(tài)如下:
/** 刷新控件的狀態(tài) */
typedef NS_ENUM(NSInteger, MJRefreshState) {
/** 普通閑置狀態(tài) */
MJRefreshStateIdle = 1,
/** 松開就可以進(jìn)行刷新的狀態(tài) */
MJRefreshStatePulling,
/** 正在刷新中的狀態(tài) */
MJRefreshStateRefreshing,
/** 即將刷新的狀態(tài) */
MJRefreshStateWillRefresh,
/** 所有數(shù)據(jù)加載完畢,沒有更多的數(shù)據(jù)了 */
MJRefreshStateNoMoreData
};
繼續(xù)往下看:
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 如果不是UIScrollView,不做任何事情
if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
// 舊的父控件移除監(jiān)聽
[self removeObservers];
if (newSuperview) { // 新的父控件
// 設(shè)置寬度
self.mj_w = newSuperview.mj_w;
// 設(shè)置位置
self.mj_x = 0;
// 記錄UIScrollView
_scrollView = (UIScrollView *)newSuperview;
// 設(shè)置永遠(yuǎn)支持垂直彈簧效果
_scrollView.alwaysBounceVertical = YES;
// 記錄UIScrollView最開始的contentInset
_scrollViewOriginalInset = _scrollView.contentInset;
// 添加監(jiān)聽
[self addObservers];
}
}
這段代碼在添加MJView的時(shí)候執(zhí)行,addObservers中設(shè)置了KVO的監(jiān)聽,具體處理如下,由于MJRefreshComponent是基類,所以并沒有做什么具體的事情,全部交由子類處理。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// 遇到這些情況就直接返回
if (!self.userInteractionEnabled) return;
// 這個(gè)就算看不見也需要處理
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{}
再往下看:
@implementation UILabel(MJRefresh)
+ (instancetype)label
{
UILabel *label = [[self alloc] init];
label.font = MJRefreshLabelFont;
label.textColor = MJRefreshLabelTextColor;
label.autoresizingMask = UIViewAutoresizingFlexibleWidth;
label.textAlignment = NSTextAlignmentCenter;
label.backgroundColor = [UIColor clearColor];
return label;
}
作者為了能快速的添加一個(gè)統(tǒng)一樣式的label,所以又給UILabel加了一個(gè)方法。
—————————————————————————
再來看看MJRefreshComponent的子類MJRefreshHeader,負(fù)責(zé)監(jiān)控用戶下拉的狀態(tài)。
pragma mark - 構(gòu)造方法
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
MJRefreshHeader *cmp = [[self alloc] init];
cmp.refreshingBlock = refreshingBlock;
return cmp;
}
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
MJRefreshHeader *cmp = [[self alloc] init];
[cmp setRefreshingTarget:target refreshingAction:action];
return cmp;
}
兩種添加header的方式,block和target方式。
繼續(xù)看:
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing狀態(tài)
if (self.state == MJRefreshStateRefreshing) {
if (self.window == nil) return;
// sectionheader停留解決
NSLog(@"%f", self.scrollView.mj_offsetY);
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;
}
// 跳轉(zhuǎn)到下一個(gè)控制器時(shí),contentInset可能會(huì)變
_scrollViewOriginalInset = self.scrollView.contentInset;
// 當(dāng)前的contentOffset
CGFloat offsetY = self.scrollView.mj_offsetY;
// 頭部控件剛好出現(xiàn)的offsetY
CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
// 如果是向上滾動(dòng)到看不見頭部控件,直接返回
// >= -> >
if (offsetY > happenOffsetY) return;
// 普通 和 即將刷新 的臨界點(diǎn)
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) {
// 轉(zhuǎn)為即將刷新狀態(tài)
self.state = MJRefreshStatePulling;
} else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
// 轉(zhuǎn)為普通狀態(tài)
self.state = MJRefreshStateIdle;
}
} else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
// 開始刷新
[self beginRefreshing];
} else if (pullingPercent < 1) {
self.pullingPercent = pullingPercent;
}
}
這里通過MJComponent的KVO觸發(fā)scrollViewContentOffsetDidChange,然后根據(jù)當(dāng)前scrollview偏移量設(shè)置控件的狀態(tài)。
再往下:
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根據(jù)狀態(tài)做事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;
// 保存刷新時(shí)間
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
[[NSUserDefaults standardUserDefaults] synchronize];
// 恢復(fù)inset和offset
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.scrollView.mj_insetT += self.insetTDelta;
// 自動(dòng)調(diào)整透明度
if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
} completion:^(BOOL finished) {
self.pullingPercent = 0.0;
}];
} else if (state == MJRefreshStateRefreshing) {
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
// 增加滾動(dòng)區(qū)域
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
self.scrollView.mj_insetT = top;
// 設(shè)置滾動(dòng)位置
self.scrollView.mj_offsetY = - top;
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
}
}
每次設(shè)置state的時(shí)候,執(zhí)行改段代碼,根據(jù)之前的狀態(tài)進(jìn)行狀態(tài)的轉(zhuǎn)換并且執(zhí)行相應(yīng)的動(dòng)畫。
另外幾個(gè)類比較簡單,MJRefreshStateHeader增加的刷新時(shí)間以及狀態(tài)等的文字顯示,MJRefreshNormalHeader增加了箭頭的顯示,MJRefreshGifHeader增加了自定義動(dòng)畫的顯示。
我們來梳理一下完整的流程:
1、調(diào)用MJRefreshHeader的構(gòu)造方法headerWithRefreshingBlock初始化Header(包括state初始化,設(shè)置KVO,獲取scrollview的gestureRecognizer),并設(shè)置回調(diào)事件;
2、手勢操作-> scrollViewContentOffsetDidChange-> setState->界面更新;