上圖為MJRefresh項目的項目結構
在MJRefresh中,使用了KVO、runtime、繼承、GCD等知識
–
MJRefreshComponent是刷新控件的基類,在MJRefreshComponent添加了KVO監聽、prepare方法和placeSubviews方法。
當MJRefreshComponent中KVO監聽到之後,響應會在MJRefreshHeader和MJRefreshFooter中實現,MJRefreshHeader和MJRefreshFooter其實響應KVO方法,主要就是設置state狀態,然後在他們的子類中會分別調用setState方法,根據不同的state狀態進行不同的變化
prepare方法和placeSubviews方法。prepare是設置刷新控件,包括文字、gif圖片、風格等等;placeSubviews是調整刷新控件的子控件的位置。在MJRefreshComponent的每一個子類中都會先調用父類對應方法,然後根據自身的特點進行不同實現
上一篇分析了MJRefresh的框架結構和核心思想,現在選擇最簡單的一個分支來進行分析。
MJRefreshNormalHeader -> MJRefreshStateHeader -> MJRefreshHeader -> MJRefreshComponent
MJRefreshNormalHeader *header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{ [self reloadData]; [self.tableView.mj_header endRefreshing]; }]; self.tableView.mj_header = header;
上面的代碼,是創建了一個MJRefreshNormalHeader的對象,然後將它賦給了self.tableView.mj_header,mj_header是什麼呢?然後找到UIScrollView+MJRefresh.h文件,可以看到這是一個分類
#import#import "MJRefreshConst.h" @class MJRefreshHeader, MJRefreshFooter; @interface UIScrollView (MJRefresh) /** 下拉刷新控件 */ @property (strong, nonatomic) MJRefreshHeader *mj_header; @property (strong, nonatomic) MJRefreshHeader *header MJRefreshDeprecated("使用mj_header"); /** 上拉刷新控件 */ @property (strong, nonatomic) MJRefreshFooter *mj_footer; @property (strong, nonatomic) MJRefreshFooter *footer MJRefreshDeprecated("使用mj_footer"); #pragma mark - other - (NSInteger)mj_totalDataCount; @property (copy, nonatomic) void (^mj_reloadDataBlock)(NSInteger totalDataCount); @end
作者利用runtime的技巧給這個分類添加了五個屬性和一個方法,然後將封裝好的刷新控件添加給UIScrollview
- (void)setMj_header:(MJRefreshHeader *)mj_header { if (mj_header != self.mj_header) { // 刪除舊的,添加新的 [self.mj_header removeFromSuperview]; [self insertSubview:mj_header atIndex:0]; // 存儲新的 [self willChangeValueForKey:@"mj_header"]; // KVO objc_setAssociatedObject(self, &MJRefreshHeaderKey, mj_header, OBJC_ASSOCIATION_ASSIGN); [self didChangeValueForKey:@"mj_header"]; // KVO } } - (void)setMj_footer:(MJRefreshFooter *)mj_footer { if (mj_footer != self.mj_footer) { // 刪除舊的,添加新的 [self.mj_footer removeFromSuperview]; [self insertSubview:mj_footer atIndex:0]; // 存儲新的 [self willChangeValueForKey:@"mj_footer"]; // KVO objc_setAssociatedObject(self, &MJRefreshFooterKey, mj_footer, OBJC_ASSOCIATION_ASSIGN); [self didChangeValueForKey:@"mj_footer"]; // KVO } }
所以其實現在可以理解,self.tableView.mj_header = header;其實就是給tableview添加一個頭部的刷新控件.而增加的屬性MJRefreshHeader就是剛才創建的MJRefreshNormalHeader的基類。MJRefreshHeader繼承於MJRefreshComponent, MJRefreshComponent是整個刷新控件的基類。
創建了MJRefreshNormalHeader的對象,直接調用了一個類方法headerWithRefreshingBlock,這個方法是它父類MJRefreshHeader的一個方法
“MJRefreshHeader.m”文件 + (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock { MJRefreshHeader *cmp = [[self alloc] init]; cmp.refreshingBlock = refreshingBlock; return cmp; }
此方法是為了創建一個MJRefreshHeader對象,在創建對象init的時候,又會調用到MJRefreshHeader的父類MJRefreshComponent的方法
@implementation MJRefreshComponent #pragma mark - 初始化 - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { // 准備工作 [self prepare]; // 默認是普通狀態 self.state = MJRefreshStateIdle; } return self; }
注意:MJRefreshComponent 類中的prepare方法,會被它的子類都進行調用,每個字類的prepare方法,都會調用父類中的prepare方法,然後增加自己特有的執行操作。
執行完init方法,最後會返回一個MJRefreshNormalHeader對象,然後添加給self.scrollview,添加上去後,便會開始執行MJRefreshComponent中的- (void)willMoveToSuperview:(UIView *)newSuperview方法
- (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]; } }
監聽了三個值,分別是UIScrollView的ContentOffSet、ContentSize、滑動手勢的狀態
#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]; }
利用KVO監聽到之後,都會響應相應的didChange方法,比如下拉刷新,下拉必然會讓contentOffSet發生變化,必然會響應對應的方法:
MJRefreshHeader文件 - (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; } }
上面的其實就是根據拖動的時候,scrollview的contentOffSet的變化進行state的設置:臨界點就是scrollView的Inset.top與刷新控件的高度相加的值。進行相應的操作,然後更改state,在每一次更改state的時候,就發生了哪些變化呢,看看下面的方法
MJRefreshHeader文件 - (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]; }]; }); } }
執行setState方法的時候,進行了界面的操作。如果是正常狀態的時候,恢復inset和offset;如果是刷新狀態,那就設置inset和offset,將scrollview的視圖往下擠一點。
再看看MJRefreshNormalHeader文件的實現
MJRefreshNormalHeader文件 #pragma mark - 重寫父類的方法 - (void)prepare { [super prepare]; self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; } - (void)placeSubviews { [super placeSubviews]; // 箭頭的中心點 CGFloat arrowCenterX = self.mj_w * 0.5; if (!self.stateLabel.hidden) { CGFloat stateWidth = self.stateLabel.mj_textWith; CGFloat timeWidth = 0.0; if (!self.lastUpdatedTimeLabel.hidden) { timeWidth = self.lastUpdatedTimeLabel.mj_textWith; } CGFloat textWidth = MAX(stateWidth, timeWidth); arrowCenterX -= textWidth / 2 + self.labelLeftInset; } CGFloat arrowCenterY = self.mj_h * 0.5; CGPoint arrowCenter = CGPointMake(arrowCenterX, arrowCenterY); // 箭頭 if (self.arrowView.constraints.count == 0) { self.arrowView.mj_size = self.arrowView.image.size; self.arrowView.center = arrowCenter; } // 圈圈 if (self.loadingView.constraints.count == 0) { self.loadingView.center = arrowCenter; } self.arrowView.tintColor = self.stateLabel.textColor; } - (void)setState:(MJRefreshState)state { MJRefreshCheckState // 根據狀態做事情 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 { [self.loadingView stopAnimating]; self.arrowView.hidden = NO; [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{ self.arrowView.transform = CGAffineTransformIdentity; }]; } } else if (state == MJRefreshStatePulling) { [self.loadingView stopAnimating]; self.arrowView.hidden = NO; [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{ self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI); }]; } else if (state == MJRefreshStateRefreshing) { self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動畫完畢動作沒有被執行 [self.loadingView startAnimating]; self.arrowView.hidden = YES; } }
上面的placeSubviews方法設置了刷新控件的子控件的位置以及大小,然後setState方法就是更加具體的根據不同state來進行界面的變換:當state由刷新變為正常時,停止loadingView的動畫,顯示箭頭;當state狀態為Pulling的時候,箭頭會發生變化,轉個方向;當state為刷新時,loadingView開始動畫,隱藏箭頭。