本篇先帶著問題來看MJRefresh,在下拉時MJRefresh是怎么使箭頭旋轉,又是如何使菊花(或其他動畫圖片)停留一段時間的呢?效果看下圖。
于是乎我對MJRefresh探究了一番,MJRefresh源碼地址,查看MJRefresh在GitHub的介紹可以得知它的主要成員如下圖:
所有的刷新控件都是繼承于基類MJRsfreshComponent的。第一步看看
MJRsfreshComponent.m
文件是怎么寫的。
一.對scrollView對象添加監聽:
#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];
}
可以看出對scrollView對象添加了KVO監聽,當scrollView有滑動手勢操作,contentOffset屬性值有變化時,進行一些處理。使得有下拉箭頭的變化和菊花的顯示,那么具體是怎么實現操作的呢,咱接著分析。
二.查看對contentOffset的監聽
先來了解一下什么是contentOffset:是scrollView基本的屬性。
contentOffset:即偏移量,其中分為contentOffset.y=內容的頂部和frame頂部的差值,contentOffset.x=內容的左邊和frame左邊的差值。
其實對contentOffset進行監聽就是看scrollView的內容是否有偏移的變化。
在MJRefreshComponent.m文件中實現了觀察監聽的方法。
- (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];
}
}
當contentOffset屬性有變化時,調用- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
方法。可以看到這個方法只是在基類MJRefreshComponent僅寫了空方法,但是在MJRefreshHeader.m文件中具體實現了這個方法
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing狀態
if (self.state == MJRefreshStateRefreshing) {
// sectionheader停留解決
...
return;
}
...
// 當前的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;
}
}
我的理解是在- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
方法實現里,提現的是對state的狀態的判斷和改變,而對- (void)setState:(MJRefreshState)state
的實現,幾個刷新空間都有自己的任務。
三.對state的改變做任務
在對MJRefreshComponent、MJRefreshHeader、MJRefreshStateHeader、MJRefreshNormalHeader四個文件查看是都有實現state的setter方法。不同的是后邊的三個子類方法里都有MJRefreshCheckState
這個宏。對狀態做了判斷和調用父類的方法。
// 狀態檢查
#define MJRefreshCheckState \
MJRefreshState oldState = self.state; \
if (state == oldState) return; \
[super setState:state]; \
分別在三個子類中添加一個行打印方法的代碼,如下:
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
NSLog(@"%s",__func__);//添加的打印方法
// 根據狀態做事情
if (state == MJRefreshStateIdle) {
...
} else if (state == MJRefreshStateRefreshing) {
...
}
}
運行demo程序,打印結果如下:
可見這四個刷新控件會依次執行- (void)setState:(MJRefreshState)state
。
主要查看MJRefreshNormalHeader文件的- (void)setState:(MJRefreshState)state
方法。
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
NSLog(@"%s",__func__);
// 根據狀態做事情
if (state == MJRefreshStateIdle) {//閑置狀態
if (oldState == MJRefreshStateRefreshing) {//正在刷新中的狀態
self.arrowView.transform = CGAffineTransformIdentity;
[...
} else {
...
}
} else if (state == MJRefreshStatePulling) {//松開就可以進行刷新的狀態
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
NSLog(@"----transform");
}];
} else if (state == MJRefreshStateRefreshing) {//正在刷新中的狀態
self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動畫完畢動作沒有被執行
[self.loadingView startAnimating];
self.arrowView.hidden = YES;
}
}
四.解答疑問
分析到這里就可以解釋文章開頭拋出的問題了:
在下拉時MJRefresh是怎么使箭頭旋轉,又是如何使菊花(或其他動畫圖片)停留一段時間的呢?
1.由于拖拽scrollview使其contentOffset發生了變化
2.在監聽contentOffset發生變化的方法里判斷偏移量的變化
3.根據偏移量的變化來設置當前state的值,即對當前的刷新狀態進行改變
4.根據state的變化來做箭頭旋轉和菊花(或其他動畫)的展示
在第四步中,有更為詳細的處理:利用UIView的+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion
來處理動畫事件。
那么菊花(或其他動畫)是如何保持一段時間然后消失的呢?
1.初始化header
在初始化tableView.mj_header
可以看到:
- (void)viewDidLoad
{
[super viewDidLoad];
...
// 下拉刷新
tableView.mj_header= [MJRefreshNormalHeader headerWithRefreshingBlock:^{
// 模擬延遲加載數據,因此2秒后才調用(真實開發中,可以移除這段gcd代碼)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 結束刷新
[tableView.mj_header endRefreshing];
});
}];
...
}
在這里是模擬延遲加載數據,當有下拉動作并松手時時,菊花會一直顯示,知道調用[tableView.mj_header endRefreshing]
。
2.調用- (void)endRefreshing
結束刷新事件
#pragma mark 結束刷新狀態
- (void)endRefreshing
{
dispatch_async(dispatch_get_main_queue(), ^{
self.state = MJRefreshStateIdle;
});
}
是把當前的刷新狀態state直接改變為普通閑置狀態
3.state改變的處理事件
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
NSLog(@"%s",__func__);
// 根據狀態做事情
if (state == MJRefreshStateIdle) {
if (oldState == MJRefreshStateRefreshing) {
self.arrowView.transform = CGAffineTransformIdentity;
//菊花停止并給隱藏
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.loadingView.alpha = 0.0;
} completion:^(BOOL finished) {
// 如果執行完動畫發現不是idle狀態,就直接返回,進入其他狀態
if (self.state != MJRefreshStateIdle) return;
self.loadingView.alpha = 1.0;
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
}];
} else {
...
} else if (state == MJRefreshStatePulling) {
...
} else if (state == MJRefreshStateRefreshing) {
...
}
}
總結:可以看出MJRefresh的刷新機制是流暢和完善的,并且是連續的完成刷新事件。在幾個刷新控件的的功能實現上各有分工,使得該開源庫讀起來簡潔并且有調理,用起來也較為方便。