最近要做一個下拉特效,由于平時用的是MJRefresh,所以研究一下MJRefresh源碼,下面把研究的一些心得寫出來
總體結構
上圖是MJRefresh在Github的圖,從上面可以看得出來,首先MJRefresh是基于一個component類的,然后是基礎的上下拉刷新控件,然后就是基于這兩個基礎類的擴展。這些基礎類在Base文件夾中定義了
本文主要探究框架內部實現原理,所以主要主要講一下基類的實現
MJRefreshComponent類
這是一個抽象類,平時使用都是使用它的子類去實現,這個類主要實現了
- 初始化
- KVO監聽
- 定義公共方法,讓子類去實現
先看初始化部分
#pragma mark - 初始化
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// 準備工作
[self prepare];
// 默認是普通狀態
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;
// 舊的父控件移除監聽
[self removeObservers];
if (newSuperview) { // 新的父控件
// 設置寬度
self.mj_w = newSuperview.mj_w;
// 設置位置
self.mj_x = 0;
// 記錄UIScrollView
_scrollView = (UIScrollView *)newSuperview;
// 設置永遠支持垂直彈簧效果
_scrollView.alwaysBounceVertical = YES;
// 記錄UIScrollView最開始的contentInset
_scrollViewOriginalInset = _scrollView.contentInset;
// 添加監聽
[self addObservers];
}
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
if (self.state == MJRefreshStateWillRefresh) {
// 預防view還沒顯示出來就調用了beginRefreshing
self.state = MJRefreshStateRefreshing;
}
}
從上面代碼段我們可以看到
-
init
方法調用了prepare
方法,prepare
方法在.h文件中暴露出來給子類實現的,主要用來初始化一些屬性(key、高度等) -
placeSubviews
方法主要用來對子視圖進行布局調整,也是給子類實現的, -
willMoveToSuperview
的注釋很詳細,主要用來判斷是不是UIScrollView類或其子類,是的話就添加監聽,所以只要是繼承UISCrollView的來都可以實現監聽 -
drawRect
方法是做一些預防處理
接下來是實現的核心,KVO監聽過程
#pragma mark - KVO監聽
- (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;
// 這個就算看不見也需要處理
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{}
從代碼段可以看到,對于scrollView的contentOffset
和contentSize
和手勢進行了監聽。當監聽的內容發生變化,就調用相應的方法;這些方法都是空的,由子類實現,上下拉根據不同需要實現不同內容。
這部分是刷新實現的核心,其本質就是通過KVO監聽scrollView的相關屬性來進行不同調用實現的。
MJRefreshHeader類
#pragma mark - 覆蓋父類的方法
- (void)prepare
{
[super prepare];
// 設置key
self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
// 設置高度
self.mj_h = MJRefreshHeaderHeight;
}
- (void)placeSubviews
{
[super placeSubviews];
// 設置y值(當自己的高度發生改變了,肯定要重新調整Y值,所以放到placeSubviews方法中設置y值)
self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}
從代碼可以看出,初始化主要是要設置key、高度和y坐標值,以(0,-高度)加入到scrollView中
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing狀態
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;
}
// 跳轉到下一個控制器時,contentInset可能會變
_scrollViewOriginalInset = self.scrollView.contentInset;
// 當前的contentOffset
CGFloat offsetY = self.scrollView.mj_offsetY;
// 頭部控件剛好出現的offsetY
CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
// 如果是向上滾動到看不見頭部控件,直接返回
// >= -> >
if (offsetY > happenOffsetY) return;
// 普通 和 即將刷新 的臨界點
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) {
// 轉為即將刷新狀態
self.state = MJRefreshStatePulling;
} else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
// 轉為普通狀態
self.state = MJRefreshStateIdle;
}
} else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
// 開始刷新
[self beginRefreshing];
} else if (pullingPercent < 1) {
self.pullingPercent = pullingPercent;
}
}
這段代碼有點多,首先if代碼段是為了解決在正在進行刷新的時候 tableView 中 sectionHeader 停留問題的;然后接下來的代碼主要就是對當前contentOffsetoffsetY
和頭部剛好出現的offsetYhappenOffsetY
的計算出刷新時所處對應的狀態,設置下拉百分比。
接下來看看狀態的set方法
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根據狀態做事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;
// 保存刷新時間
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
[[NSUserDefaults standardUserDefaults] synchronize];
// 恢復inset和offset
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.scrollView.mj_insetT += self.insetTDelta;
// 自動調整透明度
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;
// 增加滾動區域top
self.scrollView.mj_insetT = top;
// 設置滾動位置
[self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
});
}
}
從代碼可以看到,這個函數主要用于在更換狀態的時候顯示每個狀態所對應的界面,MJRefresh是通過scrollView的contentInset
來顯示數顯用的header,就是在刷險狀態的時候可以讓header停留在頂部,刷新完成后設置回原來的contentInset
一開始就有這么一個宏MJRefreshCheckState
,其實現如下
// 狀態檢查
#define MJRefreshCheckState \
MJRefreshState oldState = self.state; \
if (state == oldState) return; \
[super setState:state];
這個宏主要用來做狀態檢查的,在相同狀態下,根據上一次狀態來作不同的處理。例如剛開始往下拉處于空閑狀態要顯示一個arrow,但是下拉完成后回到空閑狀態要把arrow隱藏。
Footer實現
其實Footer的實現類似,不過具體上拉刷新有多種樣式所以需要在MJRefreshAutoFooter
MJRefreshBackFooter.h
做不同的處理,這里就不多說了
小結
整個框架在基類實現了最基本的流程,把握整個框架把這部分弄懂了就基本可以了,每個子類只是做了不同的邏輯處理而已。
總的來說,上下拉刷新的原理就是先把刷新控件添加到scrollView
的頭部或者底部,然后通過KVO監聽到scrollView
的滾動進度(底部刷新還需要監控scrollView
的內容的改變,每次改變后再次將控件調整到scrollView
的底部),根據不同的進度來設置控件的相應的文字和圖片動畫等。