MJRefresh是MJ大神寫的一個(gè)實(shí)現(xiàn)上拉刷新和下拉刷新的第三方庫(kù)健蕊,這個(gè)庫(kù)目前在很多有名的應(yīng)用上都有使用看仇味,下面就來(lái)分析一下MJRefresh的源碼。
1.簡(jiǎn)單應(yīng)用
下面創(chuàng)建一個(gè)綠色的UIScrollview,然后在UIScrollview上加上一個(gè)紅色的視圖作為子視圖:
self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 20, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
self.scrollView.backgroundColor = [UIColor greenColor];
[self.view addSubview:_scrollView];
self.scrollView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(refresh)];
self.scrollView.contentInset = UIEdgeInsetsMake(54, 0, 0, 0);
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.scrollView.frame.size.width, self.scrollView.frame.size.height)];
view.backgroundColor = [UIColor redColor];
[self.scrollView addSubview:view];
然后我們看一下效果:
在這里我們?cè)O(shè)置的contentInset.top = 54,54正是這個(gè)下拉控件的高度咱圆,所以整個(gè)下拉控件是完全可見(jiàn)的。
2.源碼分析
我們首先看一下MJRefresh的源碼的類的結(jié)構(gòu)功氨,由于上拉刷新和下拉刷新控件的原理基本一致序苏,因此這里我們僅使用下拉刷新控件來(lái)分析:下面我們從下拉刷新控件的使用開始來(lái)探索源碼:
self.scrollView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(refresh)];
首先我們的疑問(wèn)是UIScrollview的mj_header這個(gè)屬性是哪里來(lái)的?我們找到UIScrollview+MJRefresh.h
這個(gè)文件捷凄,這是寫的UIScrollview的一個(gè)分類忱详,在這個(gè)分類中我們找到了mj_header屬性,mj_footer屬性纵势,但是分類申明屬性是沒(méi)有set方法和get方法的踱阿,那么怎么去賦值和取值呢?這時(shí)候就要用到runtime的關(guān)聯(lián)屬性方法了钦铁,我們?cè)?code>UIScrollview+MJRefresh.m文件中找到- (void)setMj_header:(MJRefreshHeader *)mj_header
方法看看是不是像我猜想的那樣:
事實(shí)證明確實(shí)是這樣,使用關(guān)聯(lián)屬性來(lái)設(shè)值和取值才漆。
MJRefreshNormalHeader
最終是繼承自MJRefreshComponent
牛曹,那么我們就先從MJRefreshComponent
來(lái)看:首先在MJRefresh.h文件中有一個(gè)枚舉類表示刷新控件的狀態(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ù)加載完畢,沒(méi)有更多的數(shù)據(jù)了 */
MJRefreshStateNoMoreData
};
其中默認(rèn)狀態(tài)是MJRefreshStateIdle
醇滥, 當(dāng)我們拖拽UIScrollview的時(shí)候在沒(méi)有到達(dá)臨界點(diǎn)之前都是這個(gè)狀態(tài)黎比,當(dāng)我們拖拽到了臨界點(diǎn)之后就變成了MJRefreshStatePulling
狀態(tài)超营,這個(gè)狀態(tài)就是松手就可以刷新的狀態(tài),當(dāng)我們?cè)?code>MJRefreshStatePulling狀態(tài)下松手就變成了MJRefreshStateRefreshing
狀態(tài)阅虫,即正在刷新?tīng)顟B(tài)演闭。
MJRefreshComponnet
中有下列屬性:
-
@property (weak, nonatomic) id refreshingTarget;
這是回調(diào)的對(duì)象 -
@property (assign, nonatomic) SEL refreshingAction;
這是回調(diào)的方法 -
@property (assign, nonatomic) MJRefreshState state;
刷新控件的狀態(tài) -
@property (assign, nonatomic, readonly) UIEdgeInsets scrollViewOriginalInset;
scrollview剛開始的contentInset -
@property (assign, nonatomic) CGFloat pullingPercent;
拉拽的百分比,用來(lái)控制刷新控件的透明度
在MJRefreshComponent.m
中的核心方法是:- (void)willMoveToSuperview:(UIView *)newSuperview
颓帝,這個(gè)方法是在視圖加入父視圖或者改變父視圖的時(shí)候調(diào)用:
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 如果不是UIScrollView米碰,不做任何事情
if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
// 舊的父控件移除監(jiān)聽(tīng)
[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)聽(tīng)
[self addObservers];
}
}
在這個(gè)方法里面設(shè)置了self.mj_x,self.mj_w, _scrollViewOriginalInset
這三個(gè)屬性并且添加了觀察者:
- (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];
}
監(jiān)聽(tīng)了scrollview的contentOffset和contentsize的改變,需要根據(jù)contentoffset的改變來(lái)變更刷新控件的狀態(tài)购城。
MJRefreshHeader
接下來(lái)再來(lái)分析一下MJRefreshComponnet
的子類MJRefreshHeader
這個(gè)類,這個(gè)類是基礎(chǔ)的下拉刷新控件類:
這個(gè)類的頭文件中多了兩個(gè)方法,這兩個(gè)方法都是用來(lái)創(chuàng)建下拉刷新控件的雷酪,不同的是一個(gè)的回調(diào)是block揩慕,另一個(gè)的回調(diào)是@selector:
+ (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;
}
在這個(gè)類中確定了上拉刷新控件的mj_y屬性:
- (void)placeSubviews
{
[super placeSubviews];
// 設(shè)置y值(當(dāng)自己的高度發(fā)生改變了,肯定要重新調(diào)整Y值侮攀,所以放到placeSubviews方法中設(shè)置y值)
self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}
由于self.ignoredScrollViewContentInsetTop
一般是0锣枝,所以一般情況下就有:
self.mj_y = -self.mj_h;
然后我們?cè)賮?lái)看一下一個(gè)最核心的方法:- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
,這個(gè)方法是contentoffset發(fā)生變化時(shí)KVO產(chǎn)生的調(diào)用兰英,
上面這個(gè)方法主要是根據(jù)contentoffset進(jìn)行了一系列判斷然后進(jìn)行了狀態(tài)的變化撇叁,那么在這個(gè)類里面還有一個(gè)很重要的方法,就是
- (void)setState:(MJRefreshState)state
箭昵,我們看看這個(gè)方法里面做了什么:
總結(jié)一下
MJRefreshHeader
這個(gè)類税朴,這個(gè)類實(shí)現(xiàn)了兩個(gè)非常重要的方法,一個(gè)是- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
家制,這個(gè)方法是在用手拖拽scrollview導(dǎo)致contentoffset變化的時(shí)候調(diào)用的正林,在這個(gè)方法中會(huì)根據(jù)contentoffset的值來(lái)改變下拉刷新控件的狀態(tài)。這個(gè)類實(shí)現(xiàn)的另一個(gè)很重要的方法是- (void)setState:(MJRefreshState)state
颤殴,在- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
中改變屬性觅廓,在- (void)setState:(MJRefreshState)state
中根據(jù)屬性的改變來(lái)做具體的事。
MJRefreshStateHeader
**MJRefreshStateHeader
繼承自MJRefreshHeader
涵但,這個(gè)類是帶有狀態(tài)文字的刷新控件杈绸,沒(méi)有箭頭和菊花。這個(gè)類比較簡(jiǎn)單矮瘟,主要是對(duì)狀態(tài)label和最近刷新時(shí)間label進(jìn)行布局:
MJRefreshNoramlHeader
是MJRefreshStateHeader
的子類瞳脓,它是默認(rèn)的下拉刷新控件類,我們實(shí)例中用的就是這種澈侠,這種刷新控件是在拖拽的時(shí)候顯示箭頭劫侧,當(dāng)開始刷新的時(shí)候箭頭小時(shí),顯示菊花。
MJRefreshNoramlHeader
這個(gè)類做了兩件事烧栋,一件事是布局上面說(shuō)到的箭頭視圖和菊花視圖写妥,另外一件事是處理在拖拽過(guò)程中箭頭和菊花的變化。
-
布局菊花和箭頭:
-
處理拖拽過(guò)程中箭頭和菊花的變化
MJRefreshGifHeader
MJRefreshGifHeader
這個(gè)類也是繼承自MJRefreshStateHeader
审姓,它是帶GIF的下拉刷新控件珍特,就是把MJRefreshNormalHeader
的箭頭和菊花變成GIF,下面我們先看一下MJRefreshNormal
的簡(jiǎn)單使用:
MJRefreshGifHeader *header = [MJRefreshGifHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewData)];
// Set the ordinary state of animated images
[header setImages:idleImages forState:MJRefreshStateIdle];
// Set the pulling state of animated images(Enter the status of refreshing as soon as loosen)
[header setImages:pullingImages forState:MJRefreshStatePulling];
// Set the refreshing state of animated images
[header setImages:refreshingImages forState:MJRefreshStateRefreshing];
// Set header
self.tableView.mj_header = header;
下面我們就從- (void)setImages:(NSArray *)images forState:(MJRefreshState)state
這個(gè)方法下手來(lái)看看具體實(shí)現(xiàn):
- (void)setImages:(NSArray *)images forState:(MJRefreshState)state
{
[self setImages:images duration:images.count * 0.1 forState:state];
}
這個(gè)方法還是主要調(diào)用- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state
這個(gè)方法:
- (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;
}
}
在這個(gè)方法中魔吐,使用了兩個(gè)字典扎筒,一個(gè)字典用來(lái)存放各種狀態(tài)下的圖片數(shù)組,因?yàn)镚IF本質(zhì)上也就是多張圖片循環(huán)播放嘛画畅,另一個(gè)字典用來(lái)存放各種狀態(tài)下GIF動(dòng)畫的周期砸琅。并且如果GIF圖片的高度大于刷新控件的高度,那么就調(diào)整刷新控件的高度為GIF圖片的高度轴踱。
我們?cè)賮?lái)看一下GIF視圖的布局:
根據(jù)狀態(tài)變化顯示不同的GIF視圖:
這篇文章在簡(jiǎn)書的地址:MJRefresh源碼解讀