MJRefresh 源碼解讀

MJRefresh是李明杰老師的作品,到現在已經有9800多顆star了,是一個簡單實用,功能強大的iOS下拉刷新(也支持上拉加載更多)控件。它的可定制性很高,幾乎可以滿足大部分下拉刷新的設計需求,值得學習。

該框架的結構設計得很清晰,使用一個基類MJRefreshComponent來做一些基本的設定,然后通過繼承的方式,讓MJRefreshHeader和MJRefreshFooter分別具備下拉刷新和上拉加載的功能。從繼承機構來看可以分為三層,具體可以從下面的圖里看出來:

框架組織結構圖

首先來看一下該控件的基類:MJRefreshComponent:

MJRefreshComponent

這個類作為該控件的基類,涵蓋了基類所具備的一些:狀態,回調block等,大致分成下面這5種職能:

有哪些職能?

聲明控件的所有狀態。

聲明控件的回調函數。

添加監聽。

提供刷新,停止刷新接口。

提供子類需要實現的方法。

職能如何實現?

1. 聲明控件的所有狀態

/** 刷新控件的狀態 */typedefNS_ENUM(NSInteger, MJRefreshState) {/** 普通閑置狀態 */MJRefreshStateIdle =1,/** 松開就可以進行刷新的狀態 */MJRefreshStatePulling,/** 正在刷新中的狀態 */MJRefreshStateRefreshing,/** 即將刷新的狀態 */MJRefreshStateWillRefresh,/** 所有數據加載完畢,沒有更多的數據了 */MJRefreshStateNoMoreData};

2. 聲明控件的回調函數

/** 進入刷新狀態的回調 */typedefvoid(^MJRefreshComponentRefreshingBlock)();/** 開始刷新后的回調(進入刷新狀態后的回調) */typedefvoid(^MJRefreshComponentbeginRefreshingCompletionBlock)();/** 結束刷新后的回調 */typedefvoid(^MJRefreshComponentEndRefreshingCompletionBlock)();

3. 添加監聽

監聽的聲明:

- (void)addObservers{NSKeyValueObservingOptionsoptions =NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld;? ? [self.scrollView addObserver:selfforKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];//contentOffset屬性[self.scrollView addObserver:selfforKeyPath:MJRefreshKeyPathContentSize options:options context:nil];//contentSize屬性self.pan =self.scrollView.panGestureRecognizer;? ? [self.pan addObserver:selfforKeyPath:MJRefreshKeyPathPanState options:options context:nil];//UIPanGestureRecognizer 的state屬性}

對于監聽的處理:

- (void)observeValueForKeyPath:(NSString *)keyPathofObject:(id)objectchange:(NSDictionary *)changecontext:(void*)context{// 遇到這些情況就直接返回if(!self.userInteractionEnabled)return;// 這個就算看不見也需要處理if([keyPathisEqualToString:MJRefreshKeyPathContentSize]) {? ? ? ? [selfscrollViewContentSizeDidChange:change];? ? }// 看不見if(self.hidden)return;if([keyPathisEqualToString:MJRefreshKeyPathContentOffset]) {? ? ? ? [selfscrollViewContentOffsetDidChange:change];? ? }elseif([keyPathisEqualToString:MJRefreshKeyPathPanState]) {? ? ? ? [selfscrollViewPanStateDidChange:change];? ? }}

4. 提供刷新,停止刷新接口

#pragma mark 進入刷新狀態- (void)beginRefreshingWithCompletionBlock:(void (^)())completionBlock{self.beginRefreshingCompletionBlock = completionBlock;? ? [selfbeginRefreshing];}- (void)beginRefreshing{? ? [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{self.alpha =1.0;? ? }];self.pullingPercent =1.0;? ? // 只要正在刷新,就完全顯示? ? if (self.window) {? ? ? ? //將狀態切換為正在刷新self.state= MJRefreshStateRefreshing;? ? } else {? ? ? ? // 預防正在刷新中時,調用本方法使得header inset回置失敗? ? ? ? if (self.state!= MJRefreshStateRefreshing) {? ? ? ? ? ? //將狀態切換為即將刷新self.state= MJRefreshStateWillRefresh;? ? ? ? ? ? // 刷新(預防從另一個控制器回到這個控制器的情況,回來要重新刷新一下)? ? ? ? ? ? [selfsetNeedsDisplay];? ? ? ? }? ? }}#pragma mark 結束刷新狀態- (void)endRefreshing{self.state= MJRefreshStateIdle;}- (void)endRefreshingWithCompletionBlock:(void (^)())completionBlock{self.endRefreshingCompletionBlock = completionBlock;? ? [selfendRefreshing];}#pragma mark 是否正在刷新- (BOOL)isRefreshing{? ? returnself.state== MJRefreshStateRefreshing ||self.state== MJRefreshStateWillRefresh;}

交給子類實現的方法:

- (void)prepareNS_REQUIRES_SUPER;/** 擺放子控件frame */- (void)placeSubviewsNS_REQUIRES_SUPER;/** 當scrollView的contentOffset發生改變的時候調用 */- (void)scrollViewContentOffsetDidChange:(NSDictionary*)changeNS_REQUIRES_SUPER;/** 當scrollView的contentSize發生改變的時候調用 */- (void)scrollViewContentSizeDidChange:(NSDictionary*)changeNS_REQUIRES_SUPER;/** 當scrollView的拖拽狀態發生改變的時候調用 */- (void)scrollViewPanStateDidChange:(NSDictionary*)changeNS_REQUIRES_SUPER;

5. 提供子類需要實現的方法

#pragma mark - 交給子類們去實現/** 初始化 */- (void)prepareNS_REQUIRES_SUPER;/** 擺放子控件frame */- (void)placeSubviewsNS_REQUIRES_SUPER;/** 當scrollView的contentOffset發生改變的時候調用 */- (void)scrollViewContentOffsetDidChange:(NSDictionary*)changeNS_REQUIRES_SUPER;/** 當scrollView的contentSize發生改變的時候調用 */- (void)scrollViewContentSizeDidChange:(NSDictionary*)changeNS_REQUIRES_SUPER;/** 當scrollView的拖拽狀態發生改變的時候調用 */- (void)scrollViewPanStateDidChange:(NSDictionary*)changeNS_REQUIRES_SUPER;

從上面等結構圖可以看出,緊接著這個基類,下面分為MJRefreshHeader和MJRefreshFooter,這里順著MJRefreshHeader這個分支向下展開:

MJRefreshHeader

MJRefreshHeader繼承于MJRefreshComponent,它做了這幾件事:

有哪些職能?

初始化。

設置header高度。

重新調整y值。

根據contentOffset的變化,來切換狀態(默認狀態,可以刷新的狀態,正在刷新的狀態),實現方法是:scrollViewContentOffsetDidChange:。

在切換狀態時,執行相應的操作。實現方法是:setState:。

職能如何實現?

1. 初始化

初始化有兩種方法:

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock{? ? MJRefreshHeader *cmp= [[selfalloc] init];//傳入blockcmp.refreshingBlock= refreshingBlock;returncmp;}+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action{? ? MJRefreshHeader *cmp= [[selfalloc] init];//設置self.refreshingTarget和self.refreshingAction[cmpsetRefreshingTarget:target refreshingAction:action];returncmp;}

2. 設置header高度

通過重寫prepare方法來設置header的高度:

- (void)prepare{? ? [superprepare];// 設置用于在NSUserDefaults里存儲時間的keyself.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;// 設置header的高度self.mj_h = MJRefreshHeaderHeight;}

3. 重新調整y值

通過重寫placeSubviews方法來重新調整y值:

- (void)placeSubviews{? ? [superplaceSubviews];// 設置y值(當自己的高度發生改變了,肯定要重新調整Y值,所以放到placeSubviews方法中設置y值)self.mj_y = -self.mj_h -self.ignoredScrollViewContentInsetTop;//self.ignoredScrollViewContentInsetTop 如果是10,那么就向上移動10}

4. 狀態切換的代碼:

- (void)scrollViewContentOffsetDidChange:(NSDictionary*)change{? ? [superscrollViewContentOffsetDidChange:change];// 正在刷新的狀態if(self.state == MJRefreshStateRefreshing) {if(self.window ==nil)return;//- self.scrollView.mj_offsetY:-(-54-64)= 118 : 刷新的時候,偏移量是不動的。偏移量 = 狀態欄 + 導航欄 + header的高度//_scrollViewOriginalInset.top:64 (狀態欄 + 導航欄)//insetT 取二者之間大的那一個CGFloatinsetT = -self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? -self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;//118insetT = insetT >self.mj_h + _scrollViewOriginalInset.top ?self.mj_h + _scrollViewOriginalInset.top : insetT;//設置contentInsetself.scrollView.mj_insetT = insetT;// 記錄刷新的時候的偏移量 -54 = 64 - 118self.insetTDelta = _scrollViewOriginalInset.top - insetT;return;? ? }// 跳轉到下一個控制器時,contentInset可能會變_scrollViewOriginalInset =self.scrollView.contentInset;// 記錄當前的contentOffsetCGFloatoffsetY =self.scrollView.mj_offsetY;// 頭部控件剛好全部出現的offsetY,默認是-64(20 + 44)CGFloathappenOffsetY = -self.scrollViewOriginalInset.top;// 向上滾動,直接返回if(offsetY > happenOffsetY)return;// 從普通 到 即將刷新 的臨界距離CGFloatnormal2pullingOffsetY = happenOffsetY -self.mj_h;// -64 - 54 = -118//下拉的百分比:下拉的距離與header高度的比值CGFloatpullingPercent = (happenOffsetY - offsetY) /self.mj_h;if(self.scrollView.isDragging) {//記錄當前下拉的百分比self.pullingPercent = pullingPercent;if(self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {// 如果當前為默認狀態 && 下拉的距離大于臨界距離(將tableview下拉得很低),則將狀態切換為可以刷新self.state = MJRefreshStatePulling;? ? ? ? }elseif(self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {// 如果當前狀態為可以刷新 && 下拉的距離小于臨界距離,則將狀態切換為默認self.state = MJRefreshStateIdle;? ? ? ? }? ? }elseif(self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開// 手松開 && 狀態為可以刷新(MJRefreshStatePulling)時 開始刷新[selfbeginRefreshing];? ? }elseif(pullingPercent <1) {//手松開后,默認狀態時,恢復self.pullingPercentself.pullingPercent = pullingPercent;? ? ? ? ? ? }}

需要注意三點:

這里的狀態有三種:默認狀態(MJRefreshStateIdle),可以刷新的狀態(MJRefreshStatePulling)以及正在刷新的狀態(MJRefreshStateRefreshing)。

狀態切換的因素有兩個:一個是下拉的距離是否超過臨界值,另一個是 手指是否離開屏幕。

注意:可以刷新的狀態正在刷新的狀態是不同的。因為在手指還貼在屏幕的時候是不能進行刷新的。所以即使在下拉的距離超過了臨界距離(狀態欄 + 導航欄 + header高度),如果手指沒有離開屏幕,那么也不能馬上進行刷新,而是將狀態切換為:可以刷新。一旦手指離開了屏幕,馬上將狀態切換為正在刷新。

這里提供一張圖來體現三個狀態的不同:

三個狀態

5. 狀態切換時的相應操作:

- (void)setState:(MJRefreshState)state{? ? MJRefreshCheckStateif(state == MJRefreshStateIdle) {//============== 設置狀態為默認狀態 =============////如果當前不是正在刷新就返回,因為這個方法主要針對從正在刷新狀態(oldstate)到默認狀態if(oldState != MJRefreshStateRefreshing)return;//刷新完成后,保存刷新完成的時間[[NSUserDefaultsstandardUserDefaults] setObject:[NSDatedate] forKey:self.lastUpdatedTimeKey];? ? ? ? [[NSUserDefaultsstandardUserDefaults] synchronize];// 恢復inset和offset[UIViewanimateWithDuration:MJRefreshSlowAnimationDuration animations:^{//118 -> 64(剪去了header的高度)self.scrollView.mj_insetT +=self.insetTDelta;// 自動調整透明度if(self.isAutomaticallyChangeAlpha)self.alpha =0.0;? ? ? ? } completion:^(BOOLfinished) {self.pullingPercent =0.0;if(self.endRefreshingCompletionBlock) {//調用刷新完成的blockself.endRefreshingCompletionBlock();? ? ? ? ? ? }? ? ? ? }];? ? }elseif(state == MJRefreshStateRefreshing) {//============== 設置狀態為正在刷新狀態 =============//dispatch_async(dispatch_get_main_queue(), ^{? ? ? ? ? ? [UIViewanimateWithDuration:MJRefreshFastAnimationDuration animations:^{CGFloattop =self.scrollViewOriginalInset.top +self.mj_h;//64 + 54 (都是默認的高度)// 重新設置contentInset,top = 118self.scrollView.mj_insetT = top;// 設置滾動位置[self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];? ? ? ? ? ? } completion:^(BOOLfinished) {//調用進行刷新的block[selfexecuteRefreshingCallback];? ? ? ? ? ? }];? ? ? ? });? ? }}

這里需要注意兩點:

這里狀態的切換,主要圍繞著兩種:默認狀態和正在刷新狀態。也就是針對開始刷新結束刷新這兩個切換點。

從正在刷新狀態狀態切換為默認狀態時(結束刷新),需要記錄刷新結束的時間。因為header里面有一個默認的label是用來顯示上次刷新的時間的。

MJRefreshStateHeader

這個類是MJRefreshHeader類的子類,它做了兩件事:

有哪些職能?

簡單布局了stateLabel和lastUpdatedTimeLabel。

根據控件狀態的切換(默認狀態,正在刷新狀態),實現了這兩個label顯示的文字的切換。

給一張圖,讓大家直觀感受一下這兩個控件:

兩個Label

職能如何實現?

這個類通過覆蓋父類三個方法來實現上述兩個實現:

方法1:prepare方法

- (void)prepare{? ? [superprepare];// 初始化間距self.labelLeftInset = MJRefreshLabelLeftInset;// 初始化文字[selfsetTitle:[NSBundlemj_localizedStringForKey:MJRefreshHeaderIdleText]forState:MJRefreshStateIdle];? ? [selfsetTitle:[NSBundlemj_localizedStringForKey:MJRefreshHeaderPullingText]forState:MJRefreshStatePulling];? ? [selfsetTitle:[NSBundlemj_localizedStringForKey:MJRefreshHeaderRefreshingText]forState:MJRefreshStateRefreshing];}

在這里,將每一個狀態對應的提示文字放入一個字典里面,key是狀態的NSNumber形式

- (void)setTitle:(NSString *)titleforState:(MJRefreshState)state{? ? if (title == nil) return;self.stateTitles[@(state)] = title;self.stateLabel.text =self.stateTitles[@(self.state)];}

方法2:placeSubviews方法

- (void)placeSubviews{? ? [super placeSubviews];? ? if (self.stateLabel.hidden) return;? ? BOOL noConstrainsOnStatusLabel =self.stateLabel.constraints.count ==0;? ? if (self.lastUpdatedTimeLabel.hidden) {? ? ? ? //如果更新時間label是隱藏的,則讓狀態label撐滿整個header? ? ? ? if (noConstrainsOnStatusLabel)self.stateLabel.frame =self.bounds;? ? } else {? ? ? ? //如果更新時間label不是隱藏的,根據約束設置更新時間label和狀態label(高度各占一半)? ? ? ? CGFloatstateLabelH =self.mj_h *0.5;? ? ? ? if (noConstrainsOnStatusLabel) {self.stateLabel.mj_x =0;self.stateLabel.mj_y =0;self.stateLabel.mj_w =self.mj_w;self.stateLabel.mj_h =stateLabelH;? ? ? ? }? ? ? ? // 更新時間labelif (self.lastUpdatedTimeLabel.constraints.count ==0) {self.lastUpdatedTimeLabel.mj_x =0;self.lastUpdatedTimeLabel.mj_y =stateLabelH;self.lastUpdatedTimeLabel.mj_w =self.mj_w;self.lastUpdatedTimeLabel.mj_h =self.mj_h -self.lastUpdatedTimeLabel.mj_y;? ? ? ? }? ? }}

這里主要是對lastUpdatedTimeLabel和stateLabel進行布局。要注意lastUpdatedTimeLabel隱藏的情況。

方法3: setState:方法

- (void)setState:(MJRefreshState)state{? ? MJRefreshCheckState? ? // 設置狀態文字self.stateLabel.text =self.stateTitles[@(state)];? ? // 重新設置key(重新顯示時間)self.lastUpdatedTimeKey =self.lastUpdatedTimeKey;}

在這里,根據傳入的state的不同,在stateLabel和lastUpdatedTimeLabel里切換相應的文字。

stateLabel里的文字直接從stateTitles字典里取出即可。

lastUpdatedTimeLabel里的文字需要通過一個方法來取出即可:

- (void)setLastUpdatedTimeKey:(NSString*)lastUpdatedTimeKey{? ? [supersetLastUpdatedTimeKey:lastUpdatedTimeKey];// 如果label隱藏了,就不用再處理if(self.lastUpdatedTimeLabel.hidden)return;//根據key,從NSUserDefaults獲取對應的NSData型時間NSDate*lastUpdatedTime = [[NSUserDefaultsstandardUserDefaults] objectForKey:lastUpdatedTimeKey];// 如果有block,從block里拿來時間,這應該是用戶自定義顯示時間格式的渠道if(self.lastUpdatedTimeText) {self.lastUpdatedTimeLabel.text =self.lastUpdatedTimeText(lastUpdatedTime);return;? ? }//如果沒有block,就按照下面的默認方法顯示時間格式if(lastUpdatedTime) {// 獲得了上次更新時間// 1.獲得年月日NSCalendar*calendar = [selfcurrentCalendar];NSUIntegerunitFlags =NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute;NSDateComponents*cmp1 = [calendar components:unitFlags fromDate:lastUpdatedTime];NSDateComponents*cmp2 = [calendar components:unitFlags fromDate:[NSDatedate]];// 2.格式化日期NSDateFormatter*formatter = [[NSDateFormatteralloc] init];BOOLisToday =NO;if([cmp1 day] == [cmp2 day]) {//今天,省去年月日formatter.dateFormat =@" HH:mm";? ? ? ? ? ? isToday =YES;? ? ? ? }elseif([cmp1 year] == [cmp2 year]) {// 今年//今年,省去年,顯示月日formatter.dateFormat =@"MM-dd HH:mm";? ? ? ? }else{//其他,年月日都顯示formatter.dateFormat =@"yyyy-MM-dd HH:mm";? ? ? ? }NSString*time = [formatter stringFromDate:lastUpdatedTime];// 3.顯示日期self.lastUpdatedTimeLabel.text = [NSStringstringWithFormat:@"%@%@%@",? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [NSBundlemj_localizedStringForKey:MJRefreshHeaderLastTimeText],? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? isToday ? [NSBundlemj_localizedStringForKey:MJRefreshHeaderDateTodayText] :@"",? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? time];? ? }else{// 沒有獲得上次更新時間(應該是第一次更新或者多次更新,之前的更新都失敗了)self.lastUpdatedTimeLabel.text = [NSStringstringWithFormat:@"%@%@",? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [NSBundlemj_localizedStringForKey:MJRefreshHeaderLastTimeText],? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [NSBundlemj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];? ? }}

在這里注意兩點:

作者通過使用block來讓用戶自己定義日期現實的格式,如果用戶沒有自定義,就使用作者提供的默認格式。

在默認格式的設置里,判斷了是否是今日,是否是今年的情況。在以后設計顯示時間的labe的時候可以借鑒一下。

MJRefreshNormalHeader

有哪些職能?

MJRefreshNormalHeader 繼承于 MJRefreshStateHeader,它主要做了兩件事:

它在MJRefreshStateHeader上添加了_arrowView和loadingView。

布局了這兩個view并在Refresh控件的狀態切換的時候改變這兩個view的樣式。

還是給一張圖來直觀感受一下這兩個view:

兩個view

職能如何實現?

同MJRefreshStateHeader一樣,也是重寫了父類的三個方法:

方法1:prepare

- (void)prepare{? ? [superprepare];self.activityIndicatorViewStyle =UIActivityIndicatorViewStyleGray;}

方法2:placeSubviews

- (void)placeSubviews{? ? [super placeSubviews];? ? // 首先將箭頭的中心點x設為header寬度的一半? ? CGFloat arrowCenterX =self.mj_w *0.5;? ? if (!self.stateLabel.hidden) {? ? ? ? CGFloatstateWidth =self.stateLabel.mj_textWith;? ? ? ? CGFloat timeWidth =0.0;? ? ? ? if (!self.lastUpdatedTimeLabel.hidden) {? ? ? ? ? ? timeWidth =self.lastUpdatedTimeLabel.mj_textWith;? ? ? ? }? ? ? ? //在stateLabel里的文字寬度和更新時間里的文字寬度里取較寬的? ? ? ? CGFloat textWidth = MAX(stateWidth, timeWidth);? ? ? ? //根據self.labelLeftInset和textWidth向左移動中心點x? ? ? ? arrowCenterX -= textWidth /2+self.labelLeftInset;? ? }? ? //中心點y永遠設置為header的高度的一半? ? CGFloat arrowCenterY =self.mj_h *0.5;? ? //獲得了最終的center,而這個center同時適用于arrowView和loadingView,因為二者是不共存的。? ? 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;? ? }? ? //arrowView的色調與stateLabel的字體顏色一致self.arrowView.tintColor =self.stateLabel.textColor;}

在這里注意一點:因為stateLabel和lastUpdatedTimeLabel是上下并排分布的,而arrowView或loadingView是在這二者的左邊,所以為了避免這兩組重合,在計算arrowView或loadingView的center的時候,需要獲取stateLabel和lastUpdatedTimeLabel兩個控件的寬度并比較大小,將較大的一個作為兩個label的‘最寬距離’,再計算center,這樣一來就不會重合了。

而對于如何計算寬度,作者給出了一個方案,大家可以在以后的實踐中使用:

- (CGFloat)mj_textWith {CGFloatstringWidth =0;CGSizesize =CGSizeMake(MAXFLOAT, MAXFLOAT);if(self.text.length >0) {#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000stringWidth =[self.text? ? ? ? ? ? ? ? ? ? ? boundingRectWithSize:size? ? ? ? ? ? ? ? ? ? ? options:NSStringDrawingUsesLineFragmentOriginattributes:@{NSFontAttributeName:self.font}? ? ? ? ? ? ? ? ? ? ? context:nil].size.width;#elsestringWidth = [self.text sizeWithFont:self.font? ? ? ? ? ? ? ? ? ? ? ? ? ? constrainedToSize:size? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? lineBreakMode:NSLineBreakByCharWrapping].width;#endif}returnstringWidth;}

方法3: setState:

- (void)setState:(MJRefreshState)state{? ? MJRefreshCheckState// 根據狀態更新arrowView和loadingView的顯示if(state == MJRefreshStateIdle) {//1. 設置為默認狀態if(oldState == MJRefreshStateRefreshing) {//1.1 從正在刷新狀態中切換過來self.arrowView.transform =CGAffineTransformIdentity;? ? ? ? ? ? [UIViewanimateWithDuration:MJRefreshSlowAnimationDuration animations:^{//隱藏菊花self.loadingView.alpha =0.0;? ? ? ? ? ? } completion:^(BOOLfinished) {// 如果執行完動畫發現不是idle狀態,就直接返回,進入其他狀態if(self.state != MJRefreshStateIdle)return;//菊花停止旋轉self.loadingView.alpha =1.0;? ? ? ? ? ? ? ? [self.loadingView stopAnimating];//顯示箭頭self.arrowView.hidden =NO;? ? ? ? ? ? }];? ? ? ? }else{//1.2 從其他狀態中切換過來[self.loadingView stopAnimating];//顯示箭頭并設置為初始狀態self.arrowView.hidden =NO;? ? ? ? ? ? [UIViewanimateWithDuration:MJRefreshFastAnimationDuration animations:^{self.arrowView.transform =CGAffineTransformIdentity;? ? ? ? ? ? }];? ? ? ? }? ? }elseif(state == MJRefreshStatePulling) {//2. 設置為可以刷新狀態[self.loadingView stopAnimating];self.arrowView.hidden =NO;? ? ? ? [UIViewanimateWithDuration:MJRefreshFastAnimationDuration animations:^{//箭頭倒立self.arrowView.transform =CGAffineTransformMakeRotation(0.000001- M_PI);? ? ? ? }];? ? }elseif(state == MJRefreshStateRefreshing) {//3. 設置為正在刷新狀態self.loadingView.alpha =1.0;// 防止refreshing -> idle的動畫完畢動作沒有被執行//菊花旋轉[self.loadingView startAnimating];//隱藏arrowViewself.arrowView.hidden =YES;? ? }}

到此為止,我們已經從MJRefreshComponent到MJRefreshNormalHeader的實現過程看了一遍。可以看出,作者將prepare,placeSubviews以及setState:方法作為基類的方法,讓下面的子類去一層一層實現。

而每一層的子類,根據自身的職責,分別按照自己的方式來實現這三個方法:

MJRefreshHeader: 負責header的高度和調整header自身在外部的位置。

MJRefreshStateHeader:負責header內部的stateLabel和lastUpdatedTimeLabel的布局和不同狀態下內部文字的顯示。

MJRefreshNormalHeader:負責header內部的loadingView以及arrowView的布局和不同狀態下的顯示。

這樣做的好處是,如果想要增加某種類型的header,只要在某一層上做文章即可。例如該框架里的MJRefreshGifHeader,它和MJRefreshNormalHeader屬于同一級,都是繼承于MJRefreshStateHeader。因為二者都具有相同形式的stateLabel和lastUpdatedTimeLabel,唯一不同的就是左側的部分:

MJRefreshNormalHeader的左側是箭頭。

MJRefreshGifHeader的左側則是一個gif動畫。

還是提供一張圖來直觀感受一下:

normalHeader 與 gifHeader

下面我們來看一下的實現:

MJRefreshGifHeader

它提供了兩個接口,是用來設置不同狀態下使用的圖片數組的:

- (void)setImages:(NSArray *)images duration:(NSTimeInterval)durationforState:(MJRefreshState)state{? ? if (images == nil) return;? ? //設置不同狀態下的圖片組和持續時間self.stateImages[@(state)] = images;self.stateDurations[@(state)] = @(duration);? ? /* 根據圖片設置控件的高度 */? ? UIImage *image = [images firstObject];? ? if (image.size.height >self.mj_h) {self.mj_h = image.size.height;? ? } }- (void)setImages:(NSArray *)imagesforState:(MJRefreshState)state{? ? //如果沒有傳入duration,則根據圖片的多少來計算? ? [selfsetImages:images duration:images.count *0.1forState:state]; }

有哪些職能?

然后,和MJRefreshNormalHeader一樣,它也重寫了基類提供的三個方法來實現顯示gif圖片的職能。

職能如何實現?

1. 初始化和label的間距

- (void)prepare{? ? [superprepare];// 初始化間距self.labelLeftInset =20;}

2. 根據label的寬度和存在與否設置gif的位置

- (void)placeSubviews{? ? [super placeSubviews];? ? //如果約束存在,就立即返回? ? if (self.gifView.constraints.count) return;self.gifView.frame =self.bounds;? ? if (self.stateLabel.hidden &&self.lastUpdatedTimeLabel.hidden) {? ? ? ? //如果stateLabel和lastUpdatedTimeLabel都在隱藏狀態,將gif劇中顯示self.gifView.contentMode = UIViewContentModeCenter;? ? } else {? ? ? ? //如果stateLabel和lastUpdatedTimeLabel中至少一個存在,則根據label的寬度設置gif的位置self.gifView.contentMode = UIViewContentModeRight;? ? ? ? CGFloatstateWidth =self.stateLabel.mj_textWith;? ? ? ? CGFloat timeWidth =0.0;? ? ? ? if (!self.lastUpdatedTimeLabel.hidden) {? ? ? ? ? ? timeWidth =self.lastUpdatedTimeLabel.mj_textWith;? ? ? ? }? ? ? ? CGFloat textWidth = MAX(stateWidth, timeWidth);self.gifView.mj_w =self.mj_w *0.5- textWidth *0.5-self.labelLeftInset;? ? }}

3. 根據傳入狀態的不同來設置動畫

- (void)setState:(MJRefreshState)state{? ? MJRefreshCheckState? ? if (state== MJRefreshStatePulling ||state== MJRefreshStateRefreshing) {? ? ? ? //1. 如果傳進來的狀態是可以刷新和正在刷新? ? ? ? NSArray *images =self.stateImages[@(state)];? ? ? ? if (images.count ==0) return;? ? ? ? [self.gifView stopAnimating];? ? ? ? if (images.count ==1) {? ? ? ? ? ? //1.1單張圖片self.gifView.image = [images lastObject];? ? ? ? } else {? ? ? ? ? ? //1.2多張圖片self.gifView.animationImages = images;self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];? ? ? ? ? ? [self.gifView startAnimating];? ? ? ? }? ? } else if (state== MJRefreshStateIdle) {? ? ? ? //2.如果傳進來的狀態是默認狀態? ? ? ? [self.gifView stopAnimating];? ? }}

Footer類是用來處理上拉加載的,實現原理和下拉刷新很類似,在這里先不介紹了~

總的來說,該框架設計得非常工整:通過一個基類來定義一些狀態和一些需要子類實現的接口。通過一層一層地繼承,讓每一層的子類各司其職,只完成真正屬于自己的任務,提高了框架的可定制性,而且對于功能的擴展和bug的追蹤也很有幫助,非常值得我們參考與借鑒。

本篇文章已經同步到我個人博客:J_Knight MJRefresh 源碼解析

歡迎來參觀 ^^

本文已在版權印備案,如需轉載請訪問版權印。48422928

獲取授權

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本文轉載自J_Knight 的MJRefresh源碼解析 MJRefresh是李明杰的作品,到現在已經有9800多...
    Detective41閱讀 675評論 0 1
  • 本篇先帶著問題來看MJRefresh,在下拉時MJRefresh是怎么使箭頭旋轉,又是如何使菊花(或其他動畫圖片)...
    iOS俱哥閱讀 317評論 0 1
  • MJRefresh這個刷新控件是一款非常好用的框架,我們在使用一個框架的同時,最好能了解下它的實現原理,不管是根據...
    老馬的春天閱讀 1,090評論 1 4
  • MJRefresh是流行的下拉刷新控件,前段時間為了修復一個BUG,讀了它的源碼,本文總結一下實現的原理 下拉刷新...
    晚安的你我閱讀 458評論 0 0
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,993評論 19 139