最近要做一個(gè)下拉特效,由于平時(shí)用的是MJRefresh,所以研究一下MJRefresh源碼,下面把研究的一些心得寫出來(lái)
總體結(jié)構(gòu)

上圖是MJRefresh在Github的圖,從上面可以看得出來(lái),首先MJRefresh是基于一個(gè)component類的,然后是基礎(chǔ)的上下拉刷新控件,然后就是基于這兩個(gè)基礎(chǔ)類的擴(kuò)展。這些基礎(chǔ)類在Base文件夾中定義了

本文主要探究框架內(nèi)部實(shí)現(xiàn)原理,所以主要主要講一下基類的實(shí)現(xiàn)
MJRefreshComponent類
這是一個(gè)抽象類,平時(shí)使用都是使用它的子類去實(shí)現(xiàn),這個(gè)類主要實(shí)現(xiàn)了
- 初始化
- KVO監(jiān)聽(tīng)
- 定義公共方法,讓子類去實(shí)現(xiàn)
先看初始化部分
#pragma mark - 初始化
- (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];
}
- (void)layoutSubviews
{
[self placeSubviews];
[super layoutSubviews];
}
- (void)placeSubviews{}
- (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 = 0;
// 記錄UIScrollView
_scrollView = (UIScrollView *)newSuperview;
// 設(shè)置永遠(yuǎn)支持垂直彈簧效果
_scrollView.alwaysBounceVertical = YES;
// 記錄UIScrollView最開(kāi)始的contentInset
_scrollViewOriginalInset = _scrollView.contentInset;
// 添加監(jiān)聽(tīng)
[self addObservers];
}
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
if (self.state == MJRefreshStateWillRefresh) {
// 預(yù)防view還沒(méi)顯示出來(lái)就調(diào)用了beginRefreshing
self.state = MJRefreshStateRefreshing;
}
}
從上面代碼段我們可以看到
-
init方法調(diào)用了prepare方法,prepare方法在.h文件中暴露出來(lái)給子類實(shí)現(xiàn)的,主要用來(lái)初始化一些屬性(key、高度等) -
placeSubviews方法主要用來(lái)對(duì)子視圖進(jìn)行布局調(diào)整,也是給子類實(shí)現(xiàn)的, -
willMoveToSuperview的注釋很詳細(xì),主要用來(lái)判斷是不是UIScrollView類或其子類,是的話就添加監(jiān)聽(tīng),所以只要是繼承UISCrollView的來(lái)都可以實(shí)現(xiàn)監(jiān)聽(tīng) -
drawRect方法是做一些預(yù)防處理
接下來(lái)是實(shí)現(xiàn)的核心,KVO監(jiān)聽(tīng)過(guò)程
#pragma mark - KVO監(jiān)聽(tīng)
- (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;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// 遇到這些情況就直接返回
if (!self.userInteractionEnabled) return;
// 這個(gè)就算看不見(jiàn)也需要處理
if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
[self scrollViewContentSizeDidChange:change];
}
// 看不見(jiàn)
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{}
從代碼段可以看到,對(duì)于scrollView的contentOffset和contentSize和手勢(shì)進(jìn)行了監(jiān)聽(tīng)。當(dāng)監(jiān)聽(tīng)的內(nèi)容發(fā)生變化,就調(diào)用相應(yīng)的方法;這些方法都是空的,由子類實(shí)現(xiàn),上下拉根據(jù)不同需要實(shí)現(xiàn)不同內(nèi)容。
這部分是刷新實(shí)現(xiàn)的核心,其本質(zhì)就是通過(guò)KVO監(jiān)聽(tīng)scrollView的相關(guān)屬性來(lái)進(jìn)行不同調(diào)用實(shí)現(xiàn)的。
MJRefreshHeader類
#pragma mark - 覆蓋父類的方法
- (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;
}
從代碼可以看出,初始化主要是要設(shè)置key、高度和y坐標(biāo)值,以(0,-高度)加入到scrollView中
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing狀態(tài)
if (self.state == MJRefreshStateRefreshing) {
if (self.window == nil) return;
// sectionheader停留解決
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)到看不見(jiàn)頭部控件,直接返回
// >= -> >
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īng)顟B(tài)
self.state = MJRefreshStatePulling;
} else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
// 轉(zhuǎn)為普通狀態(tài)
self.state = MJRefreshStateIdle;
}
} else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開(kāi)
// 開(kāi)始刷新
[self beginRefreshing];
} else if (pullingPercent < 1) {
self.pullingPercent = pullingPercent;
}
}
這段代碼有點(diǎn)多,首先if代碼段是為了解決在正在進(jìn)行刷新的時(shí)候 tableView 中 sectionHeader 停留問(wèn)題的;然后接下來(lái)的代碼主要就是對(duì)當(dāng)前contentOffsetoffsetY和頭部剛好出現(xiàn)的offsetYhappenOffsetY的計(jì)算出刷新時(shí)所處對(duì)應(yīng)的狀態(tài),設(shè)置下拉百分比。
接下來(lái)看看狀態(tài)的set方法
- (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;
if (self.endRefreshingCompletionBlock) {
self.endRefreshingCompletionBlock();
}
}];
} else if (state == MJRefreshStateRefreshing) {
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
// 增加滾動(dòng)區(qū)域top
self.scrollView.mj_insetT = top;
// 設(shè)置滾動(dòng)位置
[self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
});
}
}
從代碼可以看到,這個(gè)函數(shù)主要用于在更換狀態(tài)的時(shí)候顯示每個(gè)狀態(tài)所對(duì)應(yīng)的界面,MJRefresh是通過(guò)scrollView的contentInset來(lái)顯示數(shù)顯用的header,就是在刷險(xiǎn)狀態(tài)的時(shí)候可以讓header停留在頂部,刷新完成后設(shè)置回原來(lái)的contentInset
一開(kāi)始就有這么一個(gè)宏MJRefreshCheckState,其實(shí)現(xiàn)如下
// 狀態(tài)檢查
#define MJRefreshCheckState \
MJRefreshState oldState = self.state; \
if (state == oldState) return; \
[super setState:state];
這個(gè)宏主要用來(lái)做狀態(tài)檢查的,在相同狀態(tài)下,根據(jù)上一次狀態(tài)來(lái)作不同的處理。例如剛開(kāi)始往下拉處于空閑狀態(tài)要顯示一個(gè)arrow,但是下拉完成后回到空閑狀態(tài)要把a(bǔ)rrow隱藏。
Footer實(shí)現(xiàn)
其實(shí)Footer的實(shí)現(xiàn)類似,不過(guò)具體上拉刷新有多種樣式所以需要在MJRefreshAutoFooter MJRefreshBackFooter.h做不同的處理,這里就不多說(shuō)了
小結(jié)
整個(gè)框架在基類實(shí)現(xiàn)了最基本的流程,把握整個(gè)框架把這部分弄懂了就基本可以了,每個(gè)子類只是做了不同的邏輯處理而已。
總的來(lái)說(shuō),上下拉刷新的原理就是先把刷新控件添加到scrollView的頭部或者底部,然后通過(guò)KVO監(jiān)聽(tīng)到scrollView的滾動(dòng)進(jìn)度(底部刷新還需要監(jiān)控scrollView的內(nèi)容的改變,每次改變后再次將控件調(diào)整到scrollView的底部),根據(jù)不同的進(jìn)度來(lái)設(shè)置控件的相應(yīng)的文字和圖片動(dòng)畫等。