1.前言
MJRefresh 是日常 iOS 開發(fā)中使用頻率比較高的一款下拉刷新
/上拉加載更多
的第三方控件,平時似乎沒有完整查看過源碼誓琼,此處就記錄一下探究源碼的過程吧爽醋。
注:本文已同步至 個人博客披泪。
2.使用示例
官方給的 Example 里邊提供了很多種刷新樣式 踪蹬,本文我們只以其中 2 種樣式(UITableView + 下拉刷新 動畫圖片
和 UITableView + 上拉刷新動畫圖片
)為例展開討論。
示例1:UITableView + 下拉刷新 動畫圖片
- (void)exampleA
{
// 1.設(shè)置 header
self.tableView.mj_header = [MJChiBaoZiHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewData)];
// 2.馬上進(jìn)入刷新狀態(tài)
[self.tableView.mj_header beginRefreshing];
}
- (void)loadNewData {
// 3.下載數(shù)據(jù)的操作
__weak typeof(self) weakSelf = self;
[self requestNetDataWithCompletionBlock:^(NSArray *result, BOOL isSuccess) {
// 處理返回的數(shù)據(jù)(略)
// 4.刷新表格恶迈,并結(jié)束刷新狀態(tài)
[weakSelf.tableView reloadData];
// 5.拿到當(dāng)前的下拉刷新控件涩金,
[weakSelf.tableView.mj_header endRefreshing];
}];
}
如上邊注釋所述,大概分 5 個步驟暇仲,其中 1步做、2、4奈附、5 都是 MJRefresh 的相關(guān)操作全度。
示例2:UITableView + 上拉刷新 動畫圖片
- (void)exampleB
{
// 1.設(shè)置 footer
self.tableView.mj_footer = [MJChiBaoZiFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreData)];
}
- (void)loadNewData {
// 2.下載數(shù)據(jù)的操作
__weak typeof(self) weakSelf = self;
[self requestNetDataWithCompletionBlock:^(NSArray *result, BOOL isSuccess) {
// 將返回?cái)?shù)據(jù)追加到表格的數(shù)據(jù)源(略)
// 3.刷新表格,并結(jié)束刷新狀態(tài)
[weakSelf.tableView reloadData];
// 4.拿到當(dāng)前的上拉加載更多控件斥滤,
[weakSelf.tableView.mj_footer endRefreshing];
}
}
與下拉類似将鸵,這里的 1勉盅、3、4 是 MJRefresh 的相關(guān)操作咨堤。
3. 整體思路
在開始分析源碼之前菇篡,我覺得應(yīng)該先大概了解一下這個庫的基本實(shí)現(xiàn)思路漩符,這樣看源碼的時候才不至于暈頭轉(zhuǎn)向一喘,不知所云。下面以下拉刷新
為例嗜暴,做一個簡單介紹凸克,先看下圖。
首先闷沥,當(dāng)我們給 tableView.mj_header 賦值時萎战,實(shí)際在 tableView 上添加一個子視圖即刷新控件,但是并不是添加到 tableView 的 header 里邊舆逃,因此就不會占用 tableView 的 header蚂维。
然后,對 tableView 進(jìn)行監(jiān)聽 (KVO)路狮,當(dāng) tableView 的 contentOffset 發(fā)生變化時虫啥,刷新控件會截獲到這個時機(jī),根據(jù) contentOffset 的 y 值更新刷新控件的顯示及 tableView 的 contentInset.top奄妨,整個流程見上圖涂籽。下面說說這張圖吧 O(∩_∩)O:
① 剛把刷新控件加到 tableView 上的時候,設(shè)置刷新控件的 y 值為自身高度的負(fù)值砸抛,此時改控件會被導(dǎo)航擋住评雌,當(dāng)然也可以再設(shè)置其透明度為0。
② 下拉 tableView直焙,當(dāng)刷新控件完全顯示出來(臨界點(diǎn))之前景东,是一種狀態(tài),此時松手的話奔誓,會直接彈回去耐薯。
③ 過了臨界點(diǎn),再往下拉的時候丝里,更新控件的顯示曲初,此時松手的話就開始刷新。
④ 放手刷新的時候杯聚,控件會回彈臼婆,可以加動畫,不至于那么生硬幌绍。同時執(zhí)行調(diào)用方傳入的 block颁褂,一般是請求網(wǎng)絡(luò)數(shù)據(jù)的操作故响。
⑤ 刷新過程中,要顯示該刷新控件颁独,即不讓其彈回到導(dǎo)航后邊彩届,就給 tableView.contentInset.top 增加一個控件的高度,當(dāng)然是負(fù)值誓酒。
⑥ 當(dāng)調(diào)用方請求完數(shù)據(jù)后樟蠕,手動調(diào)用 刷新控件的 endRefreshing 方法,在這個方法類里邊更新控件 UI 至初始狀態(tài)靠柑,并將 tableView.contentInset.top 減少一個控件的高度寨辩,當(dāng)然也是負(fù)值,至此歼冰,刷新結(jié)束靡狞。
以上就基本實(shí)現(xiàn)邏輯,下面開始看源碼吧隔嫡。
4. 源碼分析
下面分別探究一下下拉刷新和上拉加載更多的源碼實(shí)現(xiàn)甸怕。
通過 示例1
和 示例2
可以推測,這個框架可以大概分 2 部分腮恩,一部分是刷新控件的載體 (UIScrollView及其子類梢杭,即 tableView 和 collectionView),另一部分就是刷新控件本身庆揪,也就是所謂的 header 和 footer式曲。
4.1 刷新控件的載體
載體主要集中在下邊這幾個分類里邊
UIScrollView+MJExtension
UIScrollView+MJRefresh
UIView+MJExtension
UIView+MJExtension
UIView+MJExtension 只是為公共基類 UIView 的 frame 提供了便捷的訪問方式,包括刷新控件也會用到缸榛。
UIScrollView+MJRefresh
UIScrollView+MJRefresh 是列表基類的分類吝羞,這個文件里邊實(shí)際包含了 3 個分類,依次為
① NSObject (MJRefresh):分別提供了交換類方法和交換實(shí)例方法的工具方法:
/// 交換實(shí)例方法
+ (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));
}
② UIScrollView (MJRefresh):依次為基類 UIScrollView 添加了 header内颗、footer 和 mj_reloadDataBlock 這三個屬性钧排,并利用關(guān)聯(lián)對象添加了對應(yīng)的 setter 和 getter 實(shí)現(xiàn),關(guān)于在既有類中使用關(guān)聯(lián)對象存放自定義數(shù)據(jù)的方法均澳,可以查閱《Effective Objective-C 2.0》中第 10 條的介紹恨溜。
其中,在 mj_reloadDataBlock
的 setter
中設(shè)置關(guān)聯(lián)對象前后分別添加了 willChangeValueForKey:
和 didChangeValueForKey:
這 2 個方法找前,意在可以添加 KVO 監(jiān)聽糟袁。
- (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
}
然后提供了一個執(zhí)行 mj_reloadDataBlock
的方法 executeReloadDataBlock
:
- (void)executeReloadDataBlock
{
!self.mj_reloadDataBlock ? : self.mj_reloadDataBlock(self.mj_totalDataCount);
}
即如果設(shè)置了 mj_reloadDataBlock
,就在此執(zhí)行這個 block躺盛,我們注意到這個參數(shù) mj_totalDataCount
项戴,點(diǎn)開后發(fā)現(xiàn),原來它指的是 UITableView 或 UICollectionView 的總行數(shù)槽惫。
- (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;
}
③④ UITableView (MJRefresh) 和 UICollectionView (MJRefresh)周叮,他們都提供了下邊兩個方法:
+ (void)load
{
[self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
}
- (void)mj_reloadData
{
[self mj_reloadData];
[self executeReloadDataBlock];
}
也就是說辩撑,在程序啟動時執(zhí)行 load 方法時將列表的 reloadData
方法與自定義的 mj_reloadData
方法交換,在新方法中增加了一步操作 [self executeReloadDataBlock];
仿耽,即執(zhí)行上文提到的 mj_reloadDataBlock
合冀,這樣,當(dāng)我們執(zhí)行 tableView 的 reloadData 方法時项贺,實(shí)際執(zhí)行的就是 mj_reloadData 這個方法了君躺。
那么這個 block 是什么時候設(shè)置的呢,全局搜索了一下敬扛,發(fā)現(xiàn)只有在 MJRefreshFooter
的 willMoveToSuperview:
方法中設(shè)置過晰洒,也就是將 footer 添加到 tableView 上的時候朝抖。
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 當(dāng)前視圖被添加到父視圖上時啥箭,設(shè)置后邊的 block,即當(dāng)列表(UICollectionView 或 UITableView)行數(shù)為 0 時治宣,隱藏當(dāng)前視圖急侥。
if (newSuperview) {
// 監(jiān)聽scrollView數(shù)據(jù)的變化
if ([self.scrollView isKindOfClass:[UITableView class]] || [self.scrollView isKindOfClass:[UICollectionView class]]) {
[self.scrollView setMj_reloadDataBlock:^(NSInteger totalDataCount) {
if (self.isAutomaticallyHidden) {
self.hidden = (totalDataCount == 0);
}
}];
}
}
}
willMoveToSuperview:
是將視圖添加到父視圖或從父視圖中移除時調(diào)用的,if (newSuperview) { ... }
說明是將 footer 添加到父視圖上的時候設(shè)置這個 block 的侮邀,block 的具體實(shí)現(xiàn)是:如果需要自動隱藏坏怪,則當(dāng)數(shù)據(jù)的總條數(shù)為 0 時,隱藏 footer绊茧,否則展示铝宵。
UIScrollView+MJExtension
UIScrollView+MJExtension 是關(guān)于下邊幾個屬性的便捷訪問方式:
contentInset / adjustedContentInset
contentOffset
contentSize
其中,adjustedContentInset 是 iOS 11 新引入的一個 屬性华畏,在 iOS 11 中決定 tableView 的內(nèi)容與邊緣距離的是 adjustedContentInset 屬性鹏秋,而不是 contentInset。
4.2下拉刷新控件(refreshHeader)
先來看一張圖:
上圖就是 header 的繼承關(guān)系亡笑,示例中的 MJChiBaoZiHeader
就是繼承自 MJRefreshGifHeader
侣夷。為了描述更有條理,我們從基類開始討論吧仑乌。
MJRefreshComponent
MJRefreshComponent
是所有 header 和 footer 的基類百拓,這里定義了表示刷新狀態(tài)的枚舉 MJRefreshState 和 3 種不同的回調(diào)。
/** 刷新控件的狀態(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
};
/** 進(jìn)入刷新狀態(tài)的回調(diào) */
typedef void (^MJRefreshComponentRefreshingBlock)(void);
/** 開始刷新后的回調(diào)(進(jìn)入刷新狀態(tài)后的回調(diào)) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)(void);
/** 結(jié)束刷新后的回調(diào) */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)(void);
實(shí)際上這個類的文件里有兩個類衙传,除了自己之外還有一個 UILabel 的分類,提供了一個創(chuàng)建定制好的 Label 的類方法 mj_label
和獲取文本寬度的實(shí)例方法 mj_textWith
厕九。
+ (instancetype)mj_label
{
UILabel *label = [[self alloc] init];
label.font = MJRefreshLabelFont;
label.textColor = MJRefreshLabelTextColor;
label.autoresizingMask = UIViewAutoresizingFlexibleWidth;
label.textAlignment = NSTextAlignmentCenter;
label.backgroundColor = [UIColor clearColor];
return label;
}
- (CGFloat)mj_textWith {
CGFloat stringWidth = 0;
CGSize size = CGSizeMake(MAXFLOAT, MAXFLOAT);
if (self.text.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
stringWidth =[self.text
boundingRectWithSize:size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName:self.font}
context:nil].size.width;
#else
stringWidth = [self.text sizeWithFont:self.font
constrainedToSize:size
lineBreakMode:NSLineBreakByCharWrapping].width;
#endif
}
return stringWidth;
}
下面來看看 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];
}
初始化了幾個變量:
①初始狀態(tài)設(shè)置為普通狀態(tài)止剖,既沒有觸發(fā)刷新的情況腺阳;
②prepare
方法中設(shè)置了兩個基本屬性落君,backgroundColor
和 autoresizingMask
,autoresizingMask 的初值 UIViewAutoresizingFlexibleWidth
指的是:當(dāng)父視圖的 bounds 改變時亭引,子視圖 (即當(dāng)前視圖) 自動調(diào)整寬度绎速,以保證左、右邊距不變焙蚓。
關(guān)于布局纹冤,這里重寫了 layoutSubviews
方法,在調(diào)用 super 的方法之前增加了一步操作 placeSubviews
购公,這個方法需要子類來實(shí)現(xiàn)萌京。
- (void)layoutSubviews
{
[self placeSubviews];
[super layoutSubviews];
}
- (void)placeSubviews {
}
重寫了 willMoveToSuperview:
這個方法用于將當(dāng)前視圖添加到父視圖或從父視圖中移除時添加一些額外操作。
- (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 = -_scrollView.mj_insetL;
// 記錄UIScrollView
_scrollView = (UIScrollView *)newSuperview;
// 設(shè)置永遠(yuǎn)支持垂直彈簧效果
_scrollView.alwaysBounceVertical = YES;
// 記錄UIScrollView最開始的contentInset
_scrollViewOriginalInset = _scrollView.mj_inset;
// 添加監(jiān)聽
[self addObservers];
}
}
首先對 newSuperview 做了一層過濾知残,只有是 UIScrollView 及其子類才可以繼續(xù)往下走。
然后比庄,先將舊的監(jiān)聽移除求妹。
如果是移除當(dāng)前視圖的操作,則會跳過下邊的 if 代碼佳窑,結(jié)束這個方法的執(zhí)行制恍。如果是將當(dāng)前視圖添加父視圖上,即父視圖 newSuperview 存在時神凑,保存一些值净神,最后添加新的監(jiān)聽。
下面看看添加溉委、移除監(jiān)聽的這波操作:
- (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];
}
- (void)removeObservers
{
[self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentOffset];
[self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentSize];
[self.pan removeObserver:self forKeyPath:MJRefreshKeyPathPanState];
self.pan = nil;
}
監(jiān)聽主要是針對 self.ScrollView 即父視圖的 contentOffset鹃唯、contentSize 和 父視圖的 panGestureRecognizer 的 state。
- (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];
}
}
當(dāng)監(jiān)聽到變換時薛躬,會分別觸發(fā)對應(yīng)的處理方法(下邊 3 個)俯渤,其中 scrollViewContentOffsetDidChange:
在下拉刷新和上拉加載更多時都會用到,后邊兩個方法只在上拉加載更多時會用到型宝。
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
接下來是一批公共方法:
① 設(shè)置回調(diào)對象和回調(diào)方法八匠,提供了一種內(nèi)部的響應(yīng)方式,即使用 target-Action 的方式趴酣,執(zhí)行刷新回調(diào) executeRefreshingCallback
時候用到梨树,見下邊的內(nèi)部方法。
- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action
{
self.refreshingTarget = target;
self.refreshingAction = action;
}
#pragma mark - 內(nèi)部方法
- (void)executeRefreshingCallback
{
MJRefreshDispatchAsyncOnMainQueue({
if (self.refreshingBlock) {
self.refreshingBlock();
}
// #define MJRefreshMsgSend(...) ((void (*)(void *, SEL, UIView *))objc_msgSend)(__VA_ARGS__)
// #define MJRefreshMsgTarget(target) (__bridge void *)(target)
if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
}
if (self.beginRefreshingCompletionBlock) {
self.beginRefreshingCompletionBlock();
}
})
}
接下來是一個很重要的 setter岖寞,子類可以重寫該方法抡四,在狀態(tài)方法改變的時候,及時更新刷新控件。
- (void)setState:(MJRefreshState)state
{
_state = state;
// 加入主隊(duì)列的目的是等setState:方法調(diào)用完畢指巡、設(shè)置完文字后再去布局子控件
MJRefreshDispatchAsyncOnMainQueue([self setNeedsLayout];)
}
② 進(jìn)入刷新及結(jié)束刷新的方法淑履,每種情況分別提供了一個帶 block 和一個不帶 block 的方法,后者保存了 block 之后藻雪,又調(diào)用了前者秘噪,這個 block 的作用是用來添加刷新結(jié)束后的附加操作的。
#pragma mark 進(jìn)入刷新狀態(tài)
- (void)beginRefreshing
{
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.alpha = 1.0;
}];
self.pullingPercent = 1.0;
// 只要正在刷新勉耀,就完全顯示
if (self.window) {
self.state = MJRefreshStateRefreshing;
} else {
// 預(yù)防正在刷新中時指煎,調(diào)用本方法使得header inset回置失敗
if (self.state != MJRefreshStateRefreshing) {
self.state = MJRefreshStateWillRefresh;
// 刷新(預(yù)防從另一個控制器回到這個控制器的情況,回來要重新刷新一下)
[self setNeedsDisplay];
}
}
}
- (void)beginRefreshingWithCompletionBlock:(void (^)(void))completionBlock
{
self.beginRefreshingCompletionBlock = completionBlock;
[self beginRefreshing];
}
#pragma mark 結(jié)束刷新狀態(tài)
- (void)endRefreshing
{
MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateIdle;)
}
- (void)endRefreshingWithCompletionBlock:(void (^)(void))completionBlock
{
self.endRefreshingCompletionBlock = completionBlock;
[self endRefreshing];
}
③ 最后是根據(jù)拖拽進(jìn)度自動改變透明度的相關(guān)方法便斥,即如果需要自動改變透明度至壤,則會在拖拽過程中,將拖拽進(jìn)度時時賦值給 self.alpha枢纠。
#pragma mark 自動切換透明度
- (void)setAutoChangeAlpha:(BOOL)autoChangeAlpha
{
self.automaticallyChangeAlpha = autoChangeAlpha;
}
- (BOOL)isAutoChangeAlpha
{
return self.isAutomaticallyChangeAlpha;
}
- (void)setAutomaticallyChangeAlpha:(BOOL)automaticallyChangeAlpha
{
_automaticallyChangeAlpha = automaticallyChangeAlpha;
if (self.isRefreshing) return;
if (automaticallyChangeAlpha) {
self.alpha = self.pullingPercent;
} else {
self.alpha = 1.0;
}
}
#pragma mark 根據(jù)拖拽進(jìn)度設(shè)置透明度
- (void)setPullingPercent:(CGFloat)pullingPercent
{
_pullingPercent = pullingPercent;
// 不是正在刷新的狀態(tài)像街,而且要求自動改變透明度時,將 pullingPercent 的值給 alpha京郑,否則不再往下執(zhí)行宅广。
if (self.isRefreshing) return;
if (self.isAutomaticallyChangeAlpha) {
self.alpha = pullingPercent;
}
}
MJRefreshHeader
MJRefreshHeader
是 MJRefreshComponent
的子類葫掉,但還不是最終可以使用的類些举,還在為其子類做準(zhǔn)備。先來看看它提供的兩個構(gòu)造方法俭厚,在創(chuàng)建實(shí)例對象的同時户魏,保存了響應(yīng)的回調(diào),一個采用 block挪挤,另一個采用 target-action 的方式叼丑。
#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;
}
下邊是重寫父類的方法,首先在 prepare
和 `` 方法中設(shè)置存取刷新時刻用的 key 扛门、自身高度 mj_h 及 自身的 y 坐標(biāo) mj_y鸠信。這里出現(xiàn)了一個 ignoredScrollViewContentInsetTop
,推測是一個預(yù)留的 refreshHeader 和 tableView 之間的間隙值论寨,默認(rèn)為 0星立,需要用戶設(shè)置才會有值。
- (void)prepare
{
[super prepare];
// 設(shè)置key
self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
// 設(shè)置高度
self.mj_h = MJRefreshHeaderHeight;
}
- (void)placeSubviews
{
[super placeSubviews];
// 設(shè)置y值(當(dāng)自己的高度發(fā)生改變了葬凳,肯定要重新調(diào)整Y值绰垂,所以放到placeSubviews方法中設(shè)置y值)
self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}
然后是 2 個重要的方法,scrollViewContentOffsetDidChange:
和 setState:
火焰。
scrollViewContentOffsetDidChange:
方法主要是根據(jù)當(dāng)前狀態(tài) (self.state) 和 contentOffset 更新 self.state劲装,之所以要考慮當(dāng)前狀態(tài),是為了避免頻繁的更新 state 的值,詳見代碼注釋占业。
setState:
方法針對 進(jìn)入刷新狀態(tài)
和 從刷新恢復(fù)正常狀態(tài)
分別進(jìn)行處理绒怨,前者話,將 scrollView 的 contentInset.top 加上一個 refreshHeader 的高度谦疾,對于后者窖逗,又需要將之前加上的高度減掉,以此來控制刷新控件的懸浮狀態(tài)餐蔬。另外碎紊, 從刷新恢復(fù)正常狀態(tài)
時,保存了當(dāng)前時刻樊诺,這個是為了顯示上一次刷新時間用的仗考。
最后是 2 個公共方法,一個用來獲取保存在本地的上次刷新時間词爬,另一個是 ignoredScrollViewContentInsetTop
的setter秃嗜,同時更新了刷新控件的 y 值。
MJRefreshStateHeader
MJRefreshStateHeader
也屬于這個繼承體系中的一員顿膨,繼承自 MJRefreshHeader
锅锨,這里開始就到實(shí)用階段了:
- 在 prepare 方法中將三種狀態(tài)對應(yīng)的標(biāo)簽保存到一個可變字典(stateTitles)中,以備后邊展示恋沃。
- (void)prepare {
[super prepare];
// 初始化間距
self.labelLeftInset = MJRefreshLabelLeftInset;
// 初始化文字
// 初始未觸發(fā)刷新的狀態(tài)
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle];
// 拖拽狀態(tài)
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling];
// 刷新狀態(tài)
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing];
}
- (void)setTitle:(NSString *)title forState:(MJRefreshState)state {
if (title == nil) return;
self.stateTitles[@(state)] = title;
self.stateLabel.text = self.stateTitles[@(self.state)];
}
- 用懶加載的方式為 header 添加了兩個標(biāo)簽必搞,分別用于展示狀態(tài)提示文案和上次刷新的時間;
/** 顯示刷新狀態(tài)的label */
- (UILabel *)stateLabel {
if (!_stateLabel) {
[self addSubview:_stateLabel = [UILabel mj_label]];
}
return _stateLabel;
}
/** 顯示上一次刷新時間的label */
- (UILabel *)lastUpdatedTimeLabel {
if (!_lastUpdatedTimeLabel) {
[self addSubview:_lastUpdatedTimeLabel = [UILabel mj_label]];
}
return _lastUpdatedTimeLabel;
}
- 在
setState:
方法中為 2 個標(biāo)簽分別賦值囊咏,stateLabel 根據(jù) state 從之前保存的可變字典(stateTitles)中取值(這里作者做了本地化處理)恕洲,lastUpdatedTimeLabel 的賦值有點(diǎn)特別,他是在setLastUpdatedTimeKey:
方法中對時間進(jìn)行格式化等相關(guān)處理后賦值給 lastUpdatedTimeLabel梅割,所以每次更新 state 的時候要調(diào)用用一次self.lastUpdatedTimeKey = self.lastUpdatedTimeKey
霜第。
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 設(shè)置狀態(tài)文字
self.stateLabel.text = self.stateTitles[@(state)];
// 重新設(shè)置key(重新顯示時間)
self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}
MJRefreshGifHeader
MJRefreshGifHeader
繼承自上一個類,并增加了一個用于展示動畫圖片的 imageView 及用于保存各種狀態(tài)對應(yīng)的動畫圖片和動畫時間字典户辞。
__unsafe_unretained UIImageView *_gifView;
/** 所有狀態(tài)對應(yīng)的動畫圖片 */
@property (strong, nonatomic) NSMutableDictionary *stateImages;
/** 所有狀態(tài)對應(yīng)的動畫時間 */
@property (strong, nonatomic) NSMutableDictionary *stateDurations;
為了獲取這些動畫圖片和時間泌类,并與對應(yīng)的狀態(tài)關(guān)聯(lián)起來,提供了 2 個供外界調(diào)用的方法,圖片必須提供,時間可以不傳(第二種方法)舔亭,會去默認(rèn)值 images.count * 0.1
。
- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state
{
if (images == nil) return;
self.stateImages[@(state)] = images;
self.stateDurations[@(state)] = @(duration);
/* 根據(jù)圖片設(shè)置控件的高度 */
UIImage *image = [images firstObject];
if (image.size.height > self.mj_h) {
self.mj_h = image.size.height;
}
}
- (void)setImages:(NSArray *)images forState:(MJRefreshState)state
{
[self setImages:images duration:images.count * 0.1 forState:state];
}
這里重寫了父類的 setState:
方法喇澡,當(dāng)拖拽或刷新的時候才會去設(shè)置動畫圖片,首先停止之前的動畫殊校,然后再設(shè)置新值晴玖,如果是單張圖片,直接展示,多張情況才需要展示動畫; 如果 state 是正常狀態(tài)呕屎,則停止動畫让簿。
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根據(jù)狀態(tài)做事情
if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {
NSArray *images = self.stateImages[@(state)];
if (images.count == 0) return;
[self.gifView stopAnimating];
if (images.count == 1) { // 單張圖片
self.gifView.image = [images lastObject];
} else { // 多張圖片
self.gifView.animationImages = images;
self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
[self.gifView startAnimating];
}
} else if (state == MJRefreshStateIdle) {
[self.gifView stopAnimating];
}
}
MJChiBaoZiHeader
先來看看 prepare
的實(shí)現(xiàn),只重寫了父類的一個方法:
- (void)prepare
{
// 0.執(zhí)行父類的 prepare 方法
[super prepare];
// 1.設(shè)置普通狀態(tài)的動畫圖片
NSMutableArray *idleImages = [NSMutableArray array];
for (NSUInteger i = 1; i<=60; i++) {
UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_anim__000%zd", i]];
[idleImages addObject:image];
}
[self setImages:idleImages forState:MJRefreshStateIdle];
// 2.設(shè)置即將刷新狀態(tài)的動畫圖片(一松開就會刷新的狀態(tài))
NSMutableArray *refreshingImages = [NSMutableArray array];
for (NSUInteger i = 1; i<=3; i++) {
UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_loading_0%zd", i]];
[refreshingImages addObject:image];
}
[self setImages:refreshingImages forState:MJRefreshStatePulling];
// 3.設(shè)置正在刷新狀態(tài)的動畫圖片
[self setImages:refreshingImages forState:MJRefreshStateRefreshing];
}
具體實(shí)現(xiàn)見上邊的代碼注釋秀睛,其中 prepare
和 setImages: forState:
兩個方法均來自基類尔当,上邊已經(jīng)介紹過了。
4.3 上拉加載更多控件(refreshFooter)
與 header 類似蹂安,先看一下類的繼承關(guān)系:
既然都是繼承自 MJRefreshComponent
椭迎,這里就直接從 MJRefreshFooter
開始討論,然后準(zhǔn)者繼承體系一直講到 MJChiBaoZiFooter
田盈,即示例 2 用到的 footer畜号。
MJRefreshFooter
觀察這個類的源碼就會發(fā)現(xiàn),他和 MJRefreshHeader
有許多相似之處允瞧,比如都提供了兩個構(gòu)造方法简软,一個用 block ,一個用 target-Action述暂。下面主要說下不一樣的地方痹升。
有一個自動根據(jù)有無數(shù)據(jù)來顯示和隱藏 footer 的屬性 automaticallyHidden
,不過作者不建議使用畦韭,而且后期可能會移除疼蛾。不過,還是假名單介紹一下吧廊驼。
@property (assign, nonatomic, getter=isAutomaticallyHidden) BOOL automaticallyHidden
在 viewWillMoveToSuperView:
中設(shè)置 footer 隱藏與否的時候會用到這個屬性据过,詳見前邊 4.1刷新控件的載體
的介紹,這里不再復(fù)述妒挎。
最后看 2 個公共方法,廢棄的那個就不列出來了O(∩_∩)O:
/** 提示沒有更多的數(shù)據(jù) */
- (void)endRefreshingWithNoMoreData {
MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateNoMoreData;)
}
/** 重置沒有更多的數(shù)據(jù)(消除沒有更多數(shù)據(jù)的狀態(tài)) */
- (void)resetNoMoreData {
MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateIdle;)
}
MJRefreshAutoFooter
MJRefreshAutoFooter
是 MJRefreshFooter
的直接子類西饵,并不是可以直接使用的類酝掩,還是在為子類提供方便,這里需要重點(diǎn)介紹一下眷柔。
- 提供了幾個公開的屬性期虾,其作用見下方注釋。
#pragma mark - 公開的屬性
/** 是否自動刷新(默認(rèn)為YES驯嘱,即達(dá)到一定的觸發(fā)條件就會自動開始刷新) */
@property (assign, nonatomic, getter=isAutomaticallyRefresh) BOOL automaticallyRefresh;
/** 當(dāng)?shù)撞靠丶霈F(xiàn)多少時就自動刷新(默認(rèn)為1.0镶苞,也就是底部控件完全出現(xiàn)時,才會自動刷新) */
@property (assign, nonatomic) CGFloat triggerAutomaticallyRefreshPercent;
/** 是否每一次拖拽只發(fā)一次請求鞠评,手沒有離開屏幕的情況下反復(fù)拖拽的話茂蚓,不會觸發(fā)多次刷新 */
@property (assign, nonatomic, getter=isOnlyRefreshPerDrag) BOOL onlyRefreshPerDrag;
#pragma mark - 私有的屬性
/** 是否是一個新的拖拽 */
@property (assign, nonatomic, getter=isOneNewPan) BOOL oneNewPan;
- 重寫了
willMoveToSuperView
方法,當(dāng)添加到父控件上時,給 scrollView.contentInset.bottom 添加一個 footer 本身的高度聋涨,反之晾浴,從父控件上移除時,又要將之前加上的再減掉牍白。
- (void)willMoveToSuperview:(UIView *)newSuperview {
[super willMoveToSuperview:newSuperview];
if (newSuperview) { // 添加到父控件上
if (self.hidden == NO) {
self.scrollView.mj_insetB += self.mj_h;
}
self.mj_y = _scrollView.mj_contentH; // 設(shè)置位置
} else { // 被移除了
if (self.hidden == NO) {
self.scrollView.mj_insetB -= self.mj_h;
}
}
}
- 在準(zhǔn)備數(shù)據(jù)階段設(shè)置了這么幾個初值:
- (void)prepare {
[super prepare];
// 默認(rèn)底部控件100%出現(xiàn)時才會自動刷新
self.triggerAutomaticallyRefreshPercent = 1.0;
// 設(shè)置為默認(rèn)狀態(tài)
self.automaticallyRefresh = YES;
// 默認(rèn)是當(dāng)offset達(dá)到條件就發(fā)送請求(可連續(xù))
self.onlyRefreshPerDrag = NO;
}
- 下面是對 scrollView 的監(jiān)聽觸發(fā)的 3 個事件:
當(dāng) scrollView 的 contentSize 發(fā)生變化的時候脊凰,計(jì)時更新 footer 的 y 值,保證它一直貼著 scrollView 的下邊沿茂腥。
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
[super scrollViewContentSizeDidChange:change];
// 設(shè)置位置
self.mj_y = self.scrollView.mj_contentH;
}
當(dāng)滑動 scrollView 產(chǎn)生 contentOffset 的時候狸涌,控制當(dāng)?shù)撞克⑿驴丶耆霈F(xiàn)的時候,才能刷新最岗。
- (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) { // 內(nèi)容超過一個屏幕
// 這里的_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) {
// 防止手松開時連續(xù)調(diào)用
CGPoint old = [change[@"old"] CGPointValue];
CGPoint new = [change[@"new"] CGPointValue];
if (new.y <= old.y) return;
// 當(dāng)?shù)撞克⑿驴丶耆霈F(xiàn)時杈抢,才刷新
[self beginRefreshing];
}
}
}
當(dāng)手勢的狀態(tài)發(fā)生變化的時候,針對 UIGestureRecognizerStateEnded
和 UIGestureRecognizerStateBegan
兩種狀態(tài)進(jìn)行處理仑性,對于前者惶楼,根據(jù) contentOffset.y 決定開始刷新的時機(jī),后者的話诊杆,就認(rèn)為是一個新的手勢開始了歼捐。
- (void)scrollViewPanStateDidChange:(NSDictionary *)change
{
[super scrollViewPanStateDidChange:change];
if (self.state != MJRefreshStateIdle) return;
UIGestureRecognizerState panState = _scrollView.panGestureRecognizer.state;
if (panState == UIGestureRecognizerStateEnded) {// 手松開
if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h) { // 不夠一個屏幕
if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { // 向上拽
[self beginRefreshing];
}
} else { // 超出一個屏幕
if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView.mj_insetB - _scrollView.mj_h) {
[self beginRefreshing];
}
}
} else if (panState == UIGestureRecognizerStateBegan) {
self.oneNewPan = YES;
}
}
- 當(dāng)然也會重寫
setState
方法,如果是刷新狀態(tài)晨汹,就執(zhí)行刷新的回調(diào)豹储;如果是從刷新狀態(tài)變成沒有更多數(shù)據(jù)或停止刷新的狀態(tài),則執(zhí)行停止刷新完成的 block淘这。
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
if (state == MJRefreshStateRefreshing) {
// 刷新狀態(tài)剥扣,執(zhí)行刷新的回調(diào)
[self executeRefreshingCallback];
} else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
// 從 刷新狀態(tài) 進(jìn)入 沒有更多數(shù)據(jù)或者正常狀態(tài) 時,如果有完成后的回調(diào)铝穷,則執(zhí)行之
if (MJRefreshStateRefreshing == oldState) {
if (self.endRefreshingCompletionBlock) {
self.endRefreshingCompletionBlock();
}
}
}
}
- 最后是
setHidden:
方法钠怯,根據(jù)顯示隱藏的變化,調(diào)整contentInset
曙聂、state
和self.frame.origin.y
晦炊。
- (void)setHidden:(BOOL)hidden
{
BOOL lastHidden = self.isHidden;
[super setHidden:hidden];
// 從顯示變成隱藏狀態(tài)
if (!lastHidden && hidden) {
self.state = MJRefreshStateIdle;
self.scrollView.mj_insetB -= self.mj_h;
} else if (lastHidden && !hidden) {
// 從隱藏變成顯示狀態(tài)
self.scrollView.mj_insetB += self.mj_h;
// 設(shè)置位置
self.mj_y = _scrollView.mj_contentH;
}
}
MJRefreshAutoStateFooter
MJRefreshAutoStateFooter
繼承自 MJRefreshAutoFooter
與 MJRefreshStateHeader
類似,從這里開始介入具體的 UI宁脊,既可以直接使用了断国,這里只介紹與 MJRefreshStateHeader
不同的地方。
- 只有一個顯示刷新狀態(tài)的
stateLabel
和 保存不同狀態(tài)下文案的可變字典stateTitles
/** 顯示刷新狀態(tài)的label */
__unsafe_unretained UILabel *_stateLabel;
/** 所有狀態(tài)對應(yīng)的文字 */
@property (strong, nonatomic) NSMutableDictionary *stateTitles;
- 重寫父類
prepare
方法時榆苞,除了保存各種狀態(tài)的本地化文案外稳衬,還給 stateLabel 添加了點(diǎn)擊手勢。
- (void)prepare
{
[super prepare];
// 初始化間距
self.labelLeftInset = MJRefreshLabelLeftInset;
// 初始化文字
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshAutoFooterIdleText] forState:MJRefreshStateIdle];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshAutoFooterRefreshingText] forState:MJRefreshStateRefreshing];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshAutoFooterNoMoreDataText] forState:MJRefreshStateNoMoreData];
// 監(jiān)聽label
self.stateLabel.userInteractionEnabled = YES;
[self.stateLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(stateLabelClick)]];
}
點(diǎn)擊 stateLabel
的時候坐漏,如果是正常未刷新的狀態(tài)薄疚,則開始刷新碧信。
- (void)stateLabelClick {
if (self.state == MJRefreshStateIdle) {
[self beginRefreshing];
}
}
- 重寫
setState:
方法的時候,增加刷新過程中對stateLabel
顯示與隱藏的控制输涕。
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
if (self.isRefreshingTitleHidden && state == MJRefreshStateRefreshing) {
self.stateLabel.text = nil;
} else {
self.stateLabel.text = self.stateTitles[@(state)];
}
}
MJRefreshAutoGifFooter
MJRefreshAutoGifFooter
繼承自 MJRefreshAutoStateFooter
音婶,仔細(xì)查看其實(shí)現(xiàn)代碼,就會發(fā)現(xiàn)與 MJRefreshGifHeader
非常類似莱坎,都是在父類基礎(chǔ)上加了一個 gifView(UIImageView) 用于展示動畫圖片衣式,其他操作也基本類似,只是增加了沒有更多數(shù)據(jù)的狀態(tài) MJRefreshStateNoMoreData
以及 gifView
與 stateLabel
的顯隱控制檐什。
MJChiBaoZiFooter
MJChiBaoZiFooter
里邊也重寫了父類的 prepare
方法碴卧,調(diào)用了父類的 setImages: forState:
方法用于設(shè)置創(chuàng)新狀態(tài)時的動畫圖片,至于其方法實(shí)現(xiàn)乃正,見父類住册。
- (void)prepare
{
[super prepare];
// 設(shè)置正在刷新狀態(tài)的動畫圖片
NSMutableArray *refreshingImages = [NSMutableArray array];
for (NSUInteger i = 1; i<=3; i++) {
UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_loading_0%zd", i]];
[refreshingImages addObject:image];
}
[self setImages:refreshingImages forState:MJRefreshStateRefreshing];
}
5.小結(jié)
本文只是對 MJRefresh 源碼的一個簡單討論,很多細(xì)節(jié)還沒有講的很透瓮具,后期會及時更新這部分內(nèi)容荧飞。