MJRefresh是李明杰老師的一個開源項目,GitHub目前已經(jīng)有10000多star,GitHub地址是MJRefresh
下面我們一起來分析下MJRefresh框架的實現(xiàn)過程。
-
MJRefresh中類與類之間的聯(lián)系
mjrefresh.png - 從我們使用MJRefresh框架的調(diào)用代碼分析
eg:
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
// 屬性中的回調(diào)
}];
[self.tableView.mj_header beginRefreshing];
上面的代碼會調(diào)用MJRefreshNormalHeader父類MJRefreshStateHeader的父類MJRefreshHeader的方法:
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
// 實例化MJRefreshHeader的對象
MJRefreshHeader *cmp = [[self alloc] init];
// refreshingBlock 父類的屬性,把refreshingBlock賦值cmp.refreshingBlock屬性
cmp.refreshingBlock = refreshingBlock;
return cmp;
}
上面的headerWithRefreshingBlock:refreshingBlock;方法實例化一個一個對象cmp,會觸發(fā)MJRefreshHeader父類中的- (instancetype)initWithFrame:(CGRect)frame的方法。
#pragma mark - 初始化方法
- (instancetype)initWithFrame:(CGRect)frame
{
// 注意,此時的self 是 MJRefreshNormalHeader的對象,為什么是 MJRefreshNormalHeader的對象,設(shè)計到繼承的知識點,可以具體參考繼承,這里就不過多的說明
if (self = [super initWithFrame:frame]) {
// 調(diào)用 MJRefreshNormalHeader 中prepare方法
[self prepare];
// 默認(rèn)是普通狀態(tài),調(diào)用MJRefreshNormalHeadersetState方法
self.state = MJRefreshStateIdle;
}
return self;
}
我們回到MJRefreshNormalHeader類中的prepare方法,方法具體實現(xiàn)如下
#pragma mark - 重寫父類的方法
- (void)prepare
{
// 調(diào)用父類的 prepare 父類 是 MJRefreshStateHeader
[super prepare];
// 設(shè)置菊花樣式
self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
}
此時又會去調(diào)用MJRefreshNormalHeader父類MJRefreshStateHeader中的prepare的方法
- (void)prepare
{
[super prepare];
// 初始化間距 文字距離圈圈、箭頭的距離
self.labelLeftInset = MJRefreshLabelLeftInset;
// 初始化文字 國際化,中文,英文,繁體,
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing];
}
然后又會去調(diào)用父類中的prepare的方法,直到MJRefreshComponent類中的prepare的方法執(zhí)行完畢。關(guān)于prepare方法,里面都是做一些初始化和frame的設(shè)置,比較簡單,就不具體分析了。
再回到最開始的方法
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
// 屬性中的回調(diào)
}];
把MJRefreshNormalHeader視圖賦值給mj_header,mj_header是UIScrollView+MJRefresh類中的屬性,要給分類添加屬性,就要用到runtime機制,具體代碼如下:
#pragma mark - header
static const char MJRefreshHeaderKey = '\0';
- (void)setMj_header:(MJRefreshHeader *)mj_header
{
if (mj_header != self.mj_header) {
// 刪除舊的,添加新的
[self.mj_header removeFromSuperview];
// A insertSubView B AtIndex:2 是將B插入到A的子視圖index為2的位置(最底下是0)
// eg [self addsuview: mj_header];
[self insertSubview:mj_header atIndex:0];
// 手動kvo
[self willChangeValueForKey:@"mj_header"]; // KVO
// 給分類中的屬性添加一個set方法....
// 分類能添加屬性。但是不會自己生成getter和setter方法
objc_setAssociatedObject(self, &MJRefreshHeaderKey,
mj_header, OBJC_ASSOCIATION_ASSIGN);
// 手動kvo
[self didChangeValueForKey:@"mj_header"]; // KVO
}
}
// get方法
- (MJRefreshHeader *)mj_header
{
return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}
setMj_header的方法中,監(jiān)聽屬性用了iOS 的設(shè)計模式 KVO
[self willChangeValueForKey:@"mj_header"]; // KVO
[self didChangeValueForKey:@"mj_header"]; // KVO
為什么要用 willChangeValueForKey和didChangeValueForKey方法監(jiān)聽分類的中的屬性,而不是- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;具體可以參考KVO的在分類中的用法
[self insertSubview:mj_header atIndex:0]; A insertSubView B AtIndex:0是將B插入到A的子視圖index為0的位置。
1、這句代碼會觸發(fā)MJRefreshComponent類中的- (void)willMoveToSuperview:(nullable UIView *)newSuperview;此方法什么時候被調(diào)用?經(jīng)過查資料得知:當(dāng)視圖即將加入父視圖時或者當(dāng)視圖即將從父視圖移除時調(diào)用,具體我們分析下此方法
// newSuperview 就是父視圖 這里值得 uiscrollerView
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 如果不是UIScrollView,不做任何事情
if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
// 舊的父控件移除監(jiān)聽
[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最開始的contentInset
_scrollViewOriginalInset = _scrollView.contentInset;
;
NSLog(@"contentInset:%@",NSStringFromUIEdgeInsets(_scrollView.contentInset));
// 添加監(jiān)聽
[self addObservers];
}
}
此方法中的 self.mj_w = newSuperview.mj_w; self就是下拉的展示出來的view,mj_w是UIView+MJExtension中的屬性,實現(xiàn)的set的方法- (void)setMj_w:(CGFloat)mj_w,具體方法實現(xiàn)如下
- (void)setMj_w:(CGFloat)mj_w
{
CGRect frame = self.frame;
frame.size.width = mj_w;
self.frame = frame;
}
分析到這里,應(yīng)該明白了self.mj_w = newSuperview.mj_w;的意思了。self.mj_w = CGRectMake(original, original, newSuperview.mj_w, original);
[self addObservers];用KVO添加監(jiān)聽,給當(dāng)前的UIScrollView添加了contentOffset、contentSize、panGestureRecognizer 的監(jiān)聽
2、 [self insertSubview:mj_header atIndex:0];此方法還會觸發(fā)MJRefreshComponent 類中layoutSubviews方法,觸發(fā) layoutSubviews 有哪些操作?
找了下資料并總結(jié)下:
1、調(diào)用 addSubview 方法時會執(zhí)行該方法
2、設(shè)置并改變視圖的frame屬性時會觸發(fā)該方法
3、滑動UIScrollView及繼承與UIScrollView的控件時會觸發(fā)該方法
4、旋轉(zhuǎn)屏幕時,會觸發(fā)父視圖的layoutSubviews方法、設(shè)置并改變視圖的frame屬性時會觸發(fā)父視圖的layoutSubviews方法
OK,咱們一起看看MJRefreshComponent類中的layoutSubviews方法
- (void)layoutSubviews
{
// 此處的self依然是MJRefreshNormalHeader的對象
[self placeSubviews];
[super layoutSubviews];
}
MJRefreshNormalHeader類中的placeSubviews 添加了兩個視圖arrowView(箭頭視圖)、loadingView(菊花視圖)
MJRefreshStateHeader類中的placeSubviews 添加了兩個視圖stateLabel(狀態(tài)label )、lastUpdatedTimeLabel(顯示時間label)
MJRefreshHeader類中的placeSubviews 添加了設(shè)置了當(dāng)前視圖的Y坐標(biāo)
MJRefreshComponent類中的placeSubviews 沒有干啥 ?
鑒于placeSubviews方法比較簡單,都是關(guān)于界面的搭建,再次就不多多啰嗦了。
OK,分析到這里界面啥的都出來了。下面具體分析下拉的視圖如何出現(xiàn)
由于監(jiān)聽了UIScrollView的 contentOffset屬性,當(dāng)我們下拉的時候,觸發(fā)監(jiān)聽方法。監(jiān)聽方法在MJRefreshHeader類中
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing狀態(tài)
if (self.state == MJRefreshStateRefreshing) {
if (self.window == nil) return;
NSLog(@"%@",NSStringFromCGPoint(self.scrollView.contentOffset));
// sectionheader停留解決
//- self.scrollView.mj_offsetY:-(-54)= 54 : 刷新的時候,偏移量是不動的。偏移量 = 狀態(tài)欄 + 導(dǎo)航欄 + header的高度
//_scrollViewOriginalInset.top:64 (狀態(tài)欄 + 導(dǎo)航欄)
//insetT 取二者之間大的那一個
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;
}
NSLog(@"scrollViewContentOffsetDidChange");
// 跳轉(zhuǎn)到下一個控制器時,contentInset可能會變
_scrollViewOriginalInset = self.scrollView.contentInset;
// 當(dāng)前的contentOffset Y
CGFloat offsetY = self.scrollView.mj_offsetY;
// 頭部控件剛好出現(xiàn)的offsetY
CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
// 如果是向上滾動到看不見頭部控件,直接返回
// >= -> >
// 解釋下: offsetY 正值 就是上滑動
// offsetY 負(fù)值 就是下拉
if (offsetY > happenOffsetY) return;
// 從普通 到 即將刷新 的臨界距離 normal2pullingOffsetY = -54
CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
//下拉的百分比:下拉的距離與header高度的比值
CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
if (self.scrollView.isDragging) { // 如果正在拖拽
self.pullingPercent = pullingPercent;
if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
// 如果當(dāng)前為默認(rèn)狀態(tài) && 下拉的距離大于臨界距離(將tableview下拉得很低),則將狀態(tài)切換為可以刷新
self.state = MJRefreshStatePulling;
} else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
// 轉(zhuǎn)為普通狀態(tài)
self.state = MJRefreshStateIdle;
}
}
else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
// 開始刷新
[self beginRefreshing];
}
else if (pullingPercent < 1) {
self.pullingPercent = pullingPercent;
}
}
根據(jù)不同的state展示界面
MJRefreshStateHeader中的setState方法
- (void)setState:(MJRefreshState)state
{
// MJRefreshCheckState
// 狀態(tài)檢查
//#define MJRefreshCheckState \
MJRefreshState oldState = self.state;
if (state == oldState) return;
[super setState:state];
// 設(shè)置狀態(tài)文字
self.stateLabel.text = self.stateTitles[@(state)];
// 重新設(shè)置key(重新顯示時間)
self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}
MJRefreshNormalHeader中的setState方法
- (void)setState:(MJRefreshState)state
{
MJRefreshState oldState = self.state;
if (state == oldState) return;
[super setState:state];
// 根據(jù)狀態(tài)做事情
if (state == MJRefreshStateIdle) {
if (oldState == MJRefreshStateRefreshing) {
// 現(xiàn)在的狀態(tài)是 MJRefreshStateIdle ,上一個狀態(tài)時 MJRefreshStateRefreshing
self.arrowView.transform = CGAffineTransformIdentity;
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.loadingView.alpha = 0.0;
} completion:^(BOOL finished) {
// 如果執(zhí)行完動畫發(fā)現(xiàn)不是idle狀態(tài),就直接返回,進(jìn)入其他狀態(tài)
if (self.state != MJRefreshStateIdle) return;
self.loadingView.alpha = 1.0;
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
}];
} else {
// 當(dāng)它停止的時候,菊花視圖就會自動隱藏。
// loadingView.hidesWhenStopped = YES;
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowView.transform = CGAffineTransformIdentity;
}];
}
} else if (state == MJRefreshStatePulling) {
// loadingView 就是菊花的視圖
[self.loadingView stopAnimating];
// 箭頭視圖
self.arrowView.hidden = NO;
// 讓箭頭旋轉(zhuǎn)180°
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
}];
}
else if (state == MJRefreshStateRefreshing) {
self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動畫完畢動作沒有被執(zhí)行
[self.loadingView startAnimating];
self.arrowView.hidden = YES;
}
}
MJRefreshStateHeader中的setState方法
- (void)setState:(MJRefreshState)state
{
// MJRefreshCheckState
// 狀態(tài)檢查
//#define MJRefreshCheckState \
MJRefreshState oldState = self.state;
if (state == oldState) return;
[super setState:state];
// 設(shè)置狀態(tài)文字
self.stateLabel.text = self.stateTitles[@(state)];
// 重新設(shè)置key(重新顯示時間)
self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}
MJRefreshNormalHeader中的setState方法
- (void)setState:(MJRefreshState)state
{
// MJRefreshCheckState
// 狀態(tài)檢查
//#define MJRefreshCheckState \
MJRefreshState oldState = self.state;
if (state == oldState) return;
[super setState:state];
// 根據(jù)狀態(tài)做事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;
// 當(dāng)前的狀態(tài)必須是 MJRefreshStateIdle ,上一個狀態(tài)是 MJRefreshStateRefreshing,才可以保存時間和恢復(fù)uiscrollerView的 inset和 offset
// 保存刷新時間
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
[[NSUserDefaults standardUserDefaults] synchronize];
NSLog(@"MJRefreshState");
// 恢復(fù)inset和offset
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.scrollView.mj_insetT += self.insetTDelta;
NSLog(@"%@",NSStringFromUIEdgeInsets(self.scrollView.contentInset));
// 自動調(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) {
// 對UI的調(diào)度,都應(yīng)該在主線程中
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
// 增加滾動區(qū)域top
self.scrollView.mj_insetT = top;
// 設(shè)置滾動位置
[self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
});
}
}
MJRefreshComponent中的setState方法
- (void)setState:(MJRefreshState)state
{
_state = state;
// 加入主隊列的目的是等setState:方法調(diào)用完畢、設(shè)置完文字后再去布局子控件
dispatch_async(dispatch_get_main_queue(), ^{
[self setNeedsLayout];
});
}
關(guān)于下拉刷新,分析就到此為止,更多用法,參考MJRefreshDemo
