國內好多開發者選擇MJRefresh來實現下拉刷新,最近我也在讀他的源碼,在這我分享下我理解的實現的原理
下拉刷新的基本原理
- 一般的下拉刷新都是用<code>contentInset</code>來實現的,如果一個tableView在導航欄的正下方,那么他的<code>contentInset.top</code>就是64,<code>contentOffset.y</code>就是-64。繼續下拉<code>contentInset.top</code>不變,<code>contentOffset.y</code>變小,上拉<code>contentOffset.y</code> 變大,直到左上角達到屏幕的左上角變為0。
- 默認情況下,下拉一個tableView,在松手之后,會彈回初始的位置。而下拉刷新控件,就是將自己放在tableView的上方,初始y設置成負數,所以平時不會顯示出來,只有下拉的時候才會出現,放開又會彈回去。然后在loading的時候,臨時把<code>contentInset</code>增大,相當于把tableView往下擠,于是下拉刷新的控件就會顯示出來,然后刷新完成之后,再把<code>contentInset</code>改回原來的值,實現回彈的效果
關鍵代碼分析實現
- 從創建實例的代碼開始
[self.tableView addHeaderWithTarget:self action:@selector(loadNewData)];
[self.tableView headerEndRefreshing];
- 這是tagetAction方法,還有一種block方法,在方法中寫著真正的headerView初始化方法
- (void)addHeaderWithTarget:(id)target action:(SEL)action
{
[self addHeaderWithTarget:target action:action dateKey:nil];
}
- (void)addHeaderWithTarget:(id)target action:(SEL)action dateKey:(NSString*)dateKey
{
// 1.創建新的header
if (!self.header) {
MJRefreshHeaderView *header = [MJRefreshHeaderView header];
[self addSubview:header];
self.header = header;
}
// 2.設置目標和回調方法
self.header.beginRefreshingTaget = target;
self.header.beginRefreshingAction = action;
// 3.設置存儲刷新時間的key
self.header.dateKey = dateKey;
}
- <code>MJRefreshHeaderView *header = [MJRefreshHeaderView header]; </code>, 這個方法是第一個擴展點,具體的header有哪些屬性,哪些用戶設置的樣式都是在這里設置的,但是它現在還沒有加到任何的superView商,也沒有任何行為將其掛到tableView上,
接下來的調用<code>self.header = header;</code> ,這里運用的技巧是利用了UIScrollView+MJRefresh里的一個category,為UIScrollView增加了屬性header和footer,在set.get方法中實現,如下:
- (void)setHeader:(MJRefreshHeaderView *)header {
[self willChangeValueForKey:@"MJRefreshHeaderViewKey"];
objc_setAssociatedObject(self, &MJRefreshHeaderViewKey,
header,
OBJC_ASSOCIATION_ASSIGN);
[self didChangeValueForKey:@"MJRefreshHeaderViewKey"];
}
- (MJRefreshHeaderView *)header {
return objc_getAssociatedObject(self, &MJRefreshHeaderViewKey);
}
這里用到了關聯對象的技巧(AssociatedObject),因為category通常情況下是不能直接添加實例變量的、通過上面的代碼,把header添加到了UIScrollView的subviews里,并保留了一個引用。但是這個header的frame還沒有確定,也沒有任何行為設置header的位置和偵聽行為
<code>[self addSubview:header];</code> 由于執行了這句代碼,接下來就會進入header的生命周期方法<code>
willMoveToSuperview</code>,這個方法是在公共的基類<code>MJRefreshBaseView</code>里實現的,因為這是基礎的行為,所以寫在公共的基類里,所有的子類都能共享:
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 舊的父控件
[self.superview removeObserver:self forKeyPath:MJRefreshContentOffset context:nil];
if (newSuperview) { // 新的父控件
[newSuperview addObserver:self forKeyPath:MJRefreshContentOffset options:NSKeyValueObservingOptionNew context:nil];
// 設置寬度
self.mj_width = newSuperview.mj_width;
// 設置位置
self.mj_x = 0;
// 記錄UIScrollView
_scrollView = (UIScrollView *)newSuperview;
// 記錄UIScrollView最開始的contentInset
_scrollViewOriginalInset = _scrollView.contentInset;
}
}
- 接下來會進入生命周期方法layoutSubviews:
- (void)layoutSubviews
{
[super layoutSubviews];
// 1.箭頭
CGFloat arrowX = self.mj_width * 0.5 - 100;
self.arrowImage.center = CGPointMake(arrowX, self.mj_height * 0.5);
// 2.指示器
self.activityView.center = self.arrowImage.center;
}
- 通過上述的代碼,確定了控件的位置,以及其中每個subview的位置。
- 接下來就是偵聽<code>UIScrollView</code>的<code>contentOffset</code>和<code>contentSize</code>變化,關鍵代碼如下:
#pragma mark - 監聽UIScrollView的contentOffset屬性
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// 不能跟用戶交互就直接返回
if (!self.userInteractionEnabled || self.alpha <= 0.01 || self.hidden) return;
// 如果正在刷新,直接返回
if (self.state == MJRefreshStateRefreshing) return;
if ([MJRefreshContentOffset isEqualToString:keyPath]) {
[self adjustStateWithContentOffset];
}
}
/**
* 調整狀態
*/
- (void)adjustStateWithContentOffset
{
// 當前的contentOffset
CGFloat currentOffsetY = self.scrollView.mj_contentOffsetY;
// 頭部控件剛好出現的offsetY
CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
// 如果是向上滾動到看不見頭部控件,直接返回
if (currentOffsetY >= happenOffsetY) return;
if (self.scrollView.isDragging) {
// 普通 和 即將刷新 的臨界點
CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_height;
if (self.state == MJRefreshStateNormal && currentOffsetY < normal2pullingOffsetY) {
// 轉為即將刷新狀態
self.state = MJRefreshStatePulling;
} else if (self.state == MJRefreshStatePulling && currentOffsetY >= normal2pullingOffsetY) {
// 轉為普通狀態
self.state = MJRefreshStateNormal;
}
} else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
// 開始刷新
self.state = MJRefreshStateRefreshing;
}
}
- 接下來是設置狀態的方法,根據state狀態變化,驅動不同的行為。setState方法是第二個擴展點,這里的MJRefreshCheckState是個宏,也調用了父類的setState的方法。下拉的時候臨時增大contentInset,令header保留在屏幕上,然后調用callback block;結束之后還原contentInset,如下:
- (void)setState:(MJRefreshState)state
{
// 0.存儲當前的contentInset
if (self.state != MJRefreshStateRefreshing) {
_scrollViewOriginalInset = self.scrollView.contentInset;
}
// 1.一樣的就直接返回(暫時不返回)
if (self.state == state) return;
// 2.舊狀態
MJRefreshState oldState = self.state;
// 3.存儲狀態
_state = state;
// 4.根據狀態執行不同的操作
switch (state) {
case MJRefreshStateNormal: // 普通狀態
{
if (oldState == MJRefreshStateRefreshing) {
[UIView animateWithDuration:MJRefreshSlowAnimationDuration * 0.6 animations:^{
self.activityView.alpha = 0.0;
} completion:^(BOOL finished) {
// 停止轉圈圈
[self.activityView stopAnimating];
// 恢復alpha
self.activityView.alpha = 1.0;
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(MJRefreshSlowAnimationDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 等頭部回去
// 再次設置回normal
// _state = MJRefreshStatePulling;
// self.state = MJRefreshStateNormal;
// 顯示箭頭
self.arrowImage.hidden = NO;
// 停止轉圈圈
[self.activityView stopAnimating];
// 設置文字
[self settingLabelText];
});
// 直接返回
return;
} else {
// 顯示箭頭
self.arrowImage.hidden = NO;
// 停止轉圈圈
[self.activityView stopAnimating];
}
break;
}
case MJRefreshStatePulling:
break;
case MJRefreshStateRefreshing:
{
// 開始轉圈圈
[self.activityView startAnimating];
// 隱藏箭頭
self.arrowImage.hidden = YES;
// 回調
if ([self.beginRefreshingTaget respondsToSelector:self.beginRefreshingAction]) {
msgSend(msgTarget(self.beginRefreshingTaget), self.beginRefreshingAction, self);
}
if (self.beginRefreshingCallback) {
self.beginRefreshingCallback();
}
break;
}
default:
break;
}
// 5.設置文字
[self settingLabelText];
}
- 到此,已經將MJRefresh的原理分析的差不多了,通過分析它的原理,可以看出來下拉刷新的過程大致是:
初始化下拉刷新控件 - 設置Frame - 控件添加監聽 - 監控contentOffset - 判斷contentOffset并做出相應回調 - 希望能給各位想自己寫下拉刷新控件的帶來點幫助,有什么寫的不對、有問題的地方,希望各位能批評指正