MJRefresh是李明杰老師的作品,到現(xiàn)在已經(jīng)有9800多顆star了,是一個(gè)簡(jiǎn)單實(shí)用,功能強(qiáng)大的iOS下拉刷新(也支持上拉加載更多)控件戒幔。它的可定制性很高,幾乎可以滿足大部分下拉刷新的設(shè)計(jì)需求土童,值得學(xué)習(xí)诗茎。
該框架的結(jié)構(gòu)設(shè)計(jì)得很清晰,使用一個(gè)基類(lèi)MJRefreshComponent來(lái)做一些基本的設(shè)定献汗,然后通過(guò)繼承的方式错沃,讓MJRefreshHeader和MJRefreshFooter分別具備下拉刷新和上拉加載的功能。從繼承機(jī)構(gòu)來(lái)看可以分為三層雀瓢,具體可以從下面的圖里看出來(lái):
框架組織結(jié)構(gòu)圖
首先來(lái)看一下該控件的基類(lèi):MJRefreshComponent:
MJRefreshComponent
這個(gè)類(lèi)作為該控件的基類(lèi),涵蓋了基類(lèi)所具備的一些:狀態(tài)玉掸,回調(diào)block等刃麸,大致分成下面這5種職能:
有哪些職能?
聲明控件的所有狀態(tài)。
聲明控件的回調(diào)函數(shù)司浪。
添加監(jiān)聽(tīng)泊业。
提供刷新把沼,停止刷新接口。
提供子類(lèi)需要實(shí)現(xiàn)的方法吁伺。
職能如何實(shí)現(xiàn)饮睬?
1. 聲明控件的所有狀態(tài)
/** 刷新控件的狀態(tài) */typedefNS_ENUM(NSInteger, MJRefreshState) {/** 普通閑置狀態(tài) */MJRefreshStateIdle =1,/** 松開(kāi)就可以進(jìn)行刷新的狀態(tài) */MJRefreshStatePulling,/** 正在刷新中的狀態(tài) */MJRefreshStateRefreshing,/** 即將刷新的狀態(tài) */MJRefreshStateWillRefresh,/** 所有數(shù)據(jù)加載完畢,沒(méi)有更多的數(shù)據(jù)了 */MJRefreshStateNoMoreData};
2. 聲明控件的回調(diào)函數(shù)
/** 進(jìn)入刷新?tīng)顟B(tài)的回調(diào) */typedefvoid(^MJRefreshComponentRefreshingBlock)();/** 開(kāi)始刷新后的回調(diào)(進(jìn)入刷新?tīng)顟B(tài)后的回調(diào)) */typedefvoid(^MJRefreshComponentbeginRefreshingCompletionBlock)();/** 結(jié)束刷新后的回調(diào) */typedefvoid(^MJRefreshComponentEndRefreshingCompletionBlock)();
3. 添加監(jiān)聽(tīng)
監(jiān)聽(tīng)的聲明:
- (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屬性}
對(duì)于監(jiān)聽(tīng)的處理:
- (void)observeValueForKeyPath:(NSString *)keyPathofObject:(id)objectchange:(NSDictionary *)changecontext:(void*)context{// 遇到這些情況就直接返回if(!self.userInteractionEnabled)return;// 這個(gè)就算看不見(jiàn)也需要處理if([keyPathisEqualToString:MJRefreshKeyPathContentSize]) {? ? ? ? [selfscrollViewContentSizeDidChange:change];? ? }// 看不見(jiàn)if(self.hidden)return;if([keyPathisEqualToString:MJRefreshKeyPathContentOffset]) {? ? ? ? [selfscrollViewContentOffsetDidChange:change];? ? }elseif([keyPathisEqualToString:MJRefreshKeyPathPanState]) {? ? ? ? [selfscrollViewPanStateDidChange:change];? ? }}
4. 提供刷新篮奄,停止刷新接口
#pragma mark 進(jìn)入刷新?tīng)顟B(tài)- (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) {? ? ? ? //將狀態(tài)切換為正在刷新self.state= MJRefreshStateRefreshing;? ? } else {? ? ? ? // 預(yù)防正在刷新中時(shí),調(diào)用本方法使得header inset回置失敗? ? ? ? if (self.state!= MJRefreshStateRefreshing) {? ? ? ? ? ? //將狀態(tài)切換為即將刷新self.state= MJRefreshStateWillRefresh;? ? ? ? ? ? // 刷新(預(yù)防從另一個(gè)控制器回到這個(gè)控制器的情況窟却,回來(lái)要重新刷新一下)? ? ? ? ? ? [selfsetNeedsDisplay];? ? ? ? }? ? }}#pragma mark 結(jié)束刷新?tīng)顟B(tài)- (void)endRefreshing{self.state= MJRefreshStateIdle;}- (void)endRefreshingWithCompletionBlock:(void (^)())completionBlock{self.endRefreshingCompletionBlock = completionBlock;? ? [selfendRefreshing];}#pragma mark 是否正在刷新- (BOOL)isRefreshing{? ? returnself.state== MJRefreshStateRefreshing ||self.state== MJRefreshStateWillRefresh;}
交給子類(lèi)實(shí)現(xiàn)的方法:
- (void)prepareNS_REQUIRES_SUPER;/** 擺放子控件frame */- (void)placeSubviewsNS_REQUIRES_SUPER;/** 當(dāng)scrollView的contentOffset發(fā)生改變的時(shí)候調(diào)用 */- (void)scrollViewContentOffsetDidChange:(NSDictionary*)changeNS_REQUIRES_SUPER;/** 當(dāng)scrollView的contentSize發(fā)生改變的時(shí)候調(diào)用 */- (void)scrollViewContentSizeDidChange:(NSDictionary*)changeNS_REQUIRES_SUPER;/** 當(dāng)scrollView的拖拽狀態(tài)發(fā)生改變的時(shí)候調(diào)用 */- (void)scrollViewPanStateDidChange:(NSDictionary*)changeNS_REQUIRES_SUPER;
5. 提供子類(lèi)需要實(shí)現(xiàn)的方法
#pragma mark - 交給子類(lèi)們?nèi)?shí)現(xiàn)/** 初始化 */- (void)prepareNS_REQUIRES_SUPER;/** 擺放子控件frame */- (void)placeSubviewsNS_REQUIRES_SUPER;/** 當(dāng)scrollView的contentOffset發(fā)生改變的時(shí)候調(diào)用 */- (void)scrollViewContentOffsetDidChange:(NSDictionary*)changeNS_REQUIRES_SUPER;/** 當(dāng)scrollView的contentSize發(fā)生改變的時(shí)候調(diào)用 */- (void)scrollViewContentSizeDidChange:(NSDictionary*)changeNS_REQUIRES_SUPER;/** 當(dāng)scrollView的拖拽狀態(tài)發(fā)生改變的時(shí)候調(diào)用 */- (void)scrollViewPanStateDidChange:(NSDictionary*)changeNS_REQUIRES_SUPER;
從上面等結(jié)構(gòu)圖可以看出昼丑,緊接著這個(gè)基類(lèi),下面分為MJRefreshHeader和MJRefreshFooter,這里順著MJRefreshHeader這個(gè)分支向下展開(kāi):
MJRefreshHeader
MJRefreshHeader繼承于MJRefreshComponent夸赫,它做了這幾件事:
有哪些職能菩帝?
初始化。
設(shè)置header高度茬腿。
重新調(diào)整y值呼奢。
根據(jù)contentOffset的變化,來(lái)切換狀態(tài)(默認(rèn)狀態(tài)切平,可以刷新的狀態(tài)握础,正在刷新的狀態(tài)),實(shí)現(xiàn)方法是:scrollViewContentOffsetDidChange:揭绑。
在切換狀態(tài)時(shí)弓候,執(zhí)行相應(yīng)的操作。實(shí)現(xiàn)方法是:setState:他匪。
職能如何實(shí)現(xiàn)菇存?
1. 初始化
初始化有兩種方法:
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock{? ? MJRefreshHeader *cmp= [[selfalloc] init];//傳入blockcmp.refreshingBlock= refreshingBlock;returncmp;}+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action{? ? MJRefreshHeader *cmp= [[selfalloc] init];//設(shè)置self.refreshingTarget和self.refreshingAction[cmpsetRefreshingTarget:target refreshingAction:action];returncmp;}
2. 設(shè)置header高度
通過(guò)重寫(xiě)prepare方法來(lái)設(shè)置header的高度:
- (void)prepare{? ? [superprepare];// 設(shè)置用于在NSUserDefaults里存儲(chǔ)時(shí)間的keyself.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;// 設(shè)置header的高度self.mj_h = MJRefreshHeaderHeight;}
3. 重新調(diào)整y值
通過(guò)重寫(xiě)placeSubviews方法來(lái)重新調(diào)整y值:
- (void)placeSubviews{? ? [superplaceSubviews];// 設(shè)置y值(當(dāng)自己的高度發(fā)生改變了,肯定要重新調(diào)整Y值邦蜜,所以放到placeSubviews方法中設(shè)置y值)self.mj_y = -self.mj_h -self.ignoredScrollViewContentInsetTop;//self.ignoredScrollViewContentInsetTop 如果是10依鸥,那么就向上移動(dòng)10}
4. 狀態(tài)切換的代碼:
- (void)scrollViewContentOffsetDidChange:(NSDictionary*)change{? ? [superscrollViewContentOffsetDidChange:change];// 正在刷新的狀態(tài)if(self.state == MJRefreshStateRefreshing) {if(self.window ==nil)return;//- self.scrollView.mj_offsetY:-(-54-64)= 118 : 刷新的時(shí)候,偏移量是不動(dòng)的悼沈。偏移量 = 狀態(tài)欄 + 導(dǎo)航欄 + header的高度//_scrollViewOriginalInset.top:64 (狀態(tài)欄 + 導(dǎo)航欄)//insetT 取二者之間大的那一個(gè)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;//設(shè)置contentInsetself.scrollView.mj_insetT = insetT;// 記錄刷新的時(shí)候的偏移量 -54 = 64 - 118self.insetTDelta = _scrollViewOriginalInset.top - insetT;return;? ? }// 跳轉(zhuǎn)到下一個(gè)控制器時(shí)贱迟,contentInset可能會(huì)變_scrollViewOriginalInset =self.scrollView.contentInset;// 記錄當(dāng)前的contentOffsetCGFloatoffsetY =self.scrollView.mj_offsetY;// 頭部控件剛好全部出現(xiàn)的offsetY,默認(rèn)是-64(20 + 44)CGFloathappenOffsetY = -self.scrollViewOriginalInset.top;// 向上滾動(dòng),直接返回if(offsetY > happenOffsetY)return;// 從普通 到 即將刷新 的臨界距離CGFloatnormal2pullingOffsetY = happenOffsetY -self.mj_h;// -64 - 54 = -118//下拉的百分比:下拉的距離與header高度的比值CGFloatpullingPercent = (happenOffsetY - offsetY) /self.mj_h;if(self.scrollView.isDragging) {//記錄當(dāng)前下拉的百分比self.pullingPercent = pullingPercent;if(self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {// 如果當(dāng)前為默認(rèn)狀態(tài) && 下拉的距離大于臨界距離(將tableview下拉得很低)絮供,則將狀態(tài)切換為可以刷新self.state = MJRefreshStatePulling;? ? ? ? }elseif(self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {// 如果當(dāng)前狀態(tài)為可以刷新 && 下拉的距離小于臨界距離衣吠,則將狀態(tài)切換為默認(rèn)self.state = MJRefreshStateIdle;? ? ? ? }? ? }elseif(self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開(kāi)// 手松開(kāi) && 狀態(tài)為可以刷新(MJRefreshStatePulling)時(shí) 開(kāi)始刷新[selfbeginRefreshing];? ? }elseif(pullingPercent <1) {//手松開(kāi)后,默認(rèn)狀態(tài)時(shí)壤靶,恢復(fù)self.pullingPercentself.pullingPercent = pullingPercent;? ? ? ? ? ? }}
需要注意三點(diǎn):
這里的狀態(tài)有三種:默認(rèn)狀態(tài)(MJRefreshStateIdle)缚俏,可以刷新的狀態(tài)(MJRefreshStatePulling)以及正在刷新的狀態(tài)(MJRefreshStateRefreshing)。
狀態(tài)切換的因素有兩個(gè):一個(gè)是下拉的距離是否超過(guò)臨界值,另一個(gè)是 手指是否離開(kāi)屏幕忧换。
注意:可以刷新的狀態(tài)和正在刷新的狀態(tài)是不同的恬惯。因?yàn)樵谑种高€貼在屏幕的時(shí)候是不能進(jìn)行刷新的。所以即使在下拉的距離超過(guò)了臨界距離(狀態(tài)欄 + 導(dǎo)航欄 + header高度)亚茬,如果手指沒(méi)有離開(kāi)屏幕酪耳,那么也不能馬上進(jìn)行刷新,而是將狀態(tài)切換為:可以刷新刹缝。一旦手指離開(kāi)了屏幕碗暗,馬上將狀態(tài)切換為正在刷新。
這里提供一張圖來(lái)體現(xiàn)三個(gè)狀態(tài)的不同:
三個(gè)狀態(tài)
5. 狀態(tài)切換時(shí)的相應(yīng)操作:
- (void)setState:(MJRefreshState)state{? ? MJRefreshCheckStateif(state == MJRefreshStateIdle) {//============== 設(shè)置狀態(tài)為默認(rèn)狀態(tài) =============////如果當(dāng)前不是正在刷新就返回赞草,因?yàn)檫@個(gè)方法主要針對(duì)從正在刷新?tīng)顟B(tài)(oldstate)到默認(rèn)狀態(tài)if(oldState != MJRefreshStateRefreshing)return;//刷新完成后讹堤,保存刷新完成的時(shí)間[[NSUserDefaultsstandardUserDefaults] setObject:[NSDatedate] forKey:self.lastUpdatedTimeKey];? ? ? ? [[NSUserDefaultsstandardUserDefaults] synchronize];// 恢復(fù)inset和offset[UIViewanimateWithDuration:MJRefreshSlowAnimationDuration animations:^{//118 -> 64(剪去了header的高度)self.scrollView.mj_insetT +=self.insetTDelta;// 自動(dòng)調(diào)整透明度if(self.isAutomaticallyChangeAlpha)self.alpha =0.0;? ? ? ? } completion:^(BOOLfinished) {self.pullingPercent =0.0;if(self.endRefreshingCompletionBlock) {//調(diào)用刷新完成的blockself.endRefreshingCompletionBlock();? ? ? ? ? ? }? ? ? ? }];? ? }elseif(state == MJRefreshStateRefreshing) {//============== 設(shè)置狀態(tài)為正在刷新?tīng)顟B(tài) =============//dispatch_async(dispatch_get_main_queue(), ^{? ? ? ? ? ? [UIViewanimateWithDuration:MJRefreshFastAnimationDuration animations:^{CGFloattop =self.scrollViewOriginalInset.top +self.mj_h;//64 + 54 (都是默認(rèn)的高度)// 重新設(shè)置contentInset,top = 118self.scrollView.mj_insetT = top;// 設(shè)置滾動(dòng)位置[self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];? ? ? ? ? ? } completion:^(BOOLfinished) {//調(diào)用進(jìn)行刷新的block[selfexecuteRefreshingCallback];? ? ? ? ? ? }];? ? ? ? });? ? }}
這里需要注意兩點(diǎn):
這里狀態(tài)的切換厨疙,主要圍繞著兩種:默認(rèn)狀態(tài)和正在刷新?tīng)顟B(tài)洲守。也就是針對(duì)開(kāi)始刷新和結(jié)束刷新這兩個(gè)切換點(diǎn)。
從正在刷新?tīng)顟B(tài)狀態(tài)切換為默認(rèn)狀態(tài)時(shí)(結(jié)束刷新)沾凄,需要記錄刷新結(jié)束的時(shí)間梗醇。因?yàn)閔eader里面有一個(gè)默認(rèn)的label是用來(lái)顯示上次刷新的時(shí)間的。
MJRefreshStateHeader
這個(gè)類(lèi)是MJRefreshHeader類(lèi)的子類(lèi)撒蟀,它做了兩件事:
有哪些職能叙谨?
簡(jiǎn)單布局了stateLabel和lastUpdatedTimeLabel。
根據(jù)控件狀態(tài)的切換(默認(rèn)狀態(tài)保屯,正在刷新?tīng)顟B(tài))手负,實(shí)現(xiàn)了這兩個(gè)label顯示的文字的切換。
給一張圖姑尺,讓大家直觀感受一下這兩個(gè)控件:
兩個(gè)Label
職能如何實(shí)現(xiàn)竟终?
這個(gè)類(lèi)通過(guò)覆蓋父類(lèi)三個(gè)方法來(lái)實(shí)現(xiàn)上述兩個(gè)實(shí)現(xiàn):
方法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];}
在這里,將每一個(gè)狀態(tài)對(duì)應(yīng)的提示文字放入一個(gè)字典里面,key是狀態(tài)的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) {? ? ? ? //如果更新時(shí)間label是隱藏的切蟋,則讓狀態(tài)label撐滿整個(gè)header? ? ? ? if (noConstrainsOnStatusLabel)self.stateLabel.frame =self.bounds;? ? } else {? ? ? ? //如果更新時(shí)間label不是隱藏的统捶,根據(jù)約束設(shè)置更新時(shí)間label和狀態(tài)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;? ? ? ? }? ? ? ? // 更新時(shí)間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;? ? ? ? }? ? }}
這里主要是對(duì)lastUpdatedTimeLabel和stateLabel進(jìn)行布局。要注意lastUpdatedTimeLabel隱藏的情況柄粹。
方法3: setState:方法
- (void)setState:(MJRefreshState)state{? ? MJRefreshCheckState? ? // 設(shè)置狀態(tài)文字self.stateLabel.text =self.stateTitles[@(state)];? ? // 重新設(shè)置key(重新顯示時(shí)間)self.lastUpdatedTimeKey =self.lastUpdatedTimeKey;}
在這里喘鸟,根據(jù)傳入的state的不同,在stateLabel和lastUpdatedTimeLabel里切換相應(yīng)的文字驻右。
stateLabel里的文字直接從stateTitles字典里取出即可什黑。
lastUpdatedTimeLabel里的文字需要通過(guò)一個(gè)方法來(lái)取出即可:
- (void)setLastUpdatedTimeKey:(NSString*)lastUpdatedTimeKey{? ? [supersetLastUpdatedTimeKey:lastUpdatedTimeKey];// 如果label隱藏了,就不用再處理if(self.lastUpdatedTimeLabel.hidden)return;//根據(jù)key堪夭,從NSUserDefaults獲取對(duì)應(yīng)的NSData型時(shí)間NSDate*lastUpdatedTime = [[NSUserDefaultsstandardUserDefaults] objectForKey:lastUpdatedTimeKey];// 如果有block兑凿,從block里拿來(lái)時(shí)間凯力,這應(yīng)該是用戶自定義顯示時(shí)間格式的渠道if(self.lastUpdatedTimeText) {self.lastUpdatedTimeLabel.text =self.lastUpdatedTimeText(lastUpdatedTime);return;? ? }//如果沒(méi)有block,就按照下面的默認(rèn)方法顯示時(shí)間格式if(lastUpdatedTime) {// 獲得了上次更新時(shí)間// 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]) {//今天礼华,省去年月日f(shuō)ormatter.dateFormat =@" HH:mm";? ? ? ? ? ? isToday =YES;? ? ? ? }elseif([cmp1 year] == [cmp2 year]) {// 今年//今年,省去年拗秘,顯示月日f(shuō)ormatter.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{// 沒(méi)有獲得上次更新時(shí)間(應(yīng)該是第一次更新或者多次更新,之前的更新都失敗了)self.lastUpdatedTimeLabel.text = [NSStringstringWithFormat:@"%@%@",? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [NSBundlemj_localizedStringForKey:MJRefreshHeaderLastTimeText],? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [NSBundlemj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];? ? }}
在這里注意兩點(diǎn):
作者通過(guò)使用block來(lái)讓用戶自己定義日期現(xiàn)實(shí)的格式雕旨,如果用戶沒(méi)有自定義扮匠,就使用作者提供的默認(rèn)格式。
在默認(rèn)格式的設(shè)置里凡涩,判斷了是否是今日棒搜,是否是今年的情況。在以后設(shè)計(jì)顯示時(shí)間的labe的時(shí)候可以借鑒一下活箕。
MJRefreshNormalHeader
有哪些職能力麸?
MJRefreshNormalHeader 繼承于 MJRefreshStateHeader,它主要做了兩件事:
它在MJRefreshStateHeader上添加了_arrowView和loadingView育韩。
布局了這兩個(gè)view并在Refresh控件的狀態(tài)切換的時(shí)候改變這兩個(gè)view的樣式克蚂。
還是給一張圖來(lái)直觀感受一下這兩個(gè)view:
兩個(gè)view
職能如何實(shí)現(xiàn)?
同MJRefreshStateHeader一樣筋讨,也是重寫(xiě)了父類(lèi)的三個(gè)方法:
方法1:prepare
- (void)prepare{? ? [superprepare];self.activityIndicatorViewStyle =UIActivityIndicatorViewStyleGray;}
方法2:placeSubviews
- (void)placeSubviews{? ? [super placeSubviews];? ? // 首先將箭頭的中心點(diǎn)x設(shè)為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里的文字寬度和更新時(shí)間里的文字寬度里取較寬的? ? ? ? CGFloat textWidth = MAX(stateWidth, timeWidth);? ? ? ? //根據(jù)self.labelLeftInset和textWidth向左移動(dòng)中心點(diǎn)x? ? ? ? arrowCenterX -= textWidth /2+self.labelLeftInset;? ? }? ? //中心點(diǎn)y永遠(yuǎn)設(shè)置為header的高度的一半? ? CGFloat arrowCenterY =self.mj_h *0.5;? ? //獲得了最終的center埃叭,而這個(gè)center同時(shí)適用于arrowView和loadingView,因?yàn)槎呤遣还泊娴摹? ? 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的色調(diào)與stateLabel的字體顏色一致self.arrowView.tintColor =self.stateLabel.textColor;}
在這里注意一點(diǎn):因?yàn)閟tateLabel和lastUpdatedTimeLabel是上下并排分布的悉罕,而arrowView或loadingView是在這二者的左邊赤屋,所以為了避免這兩組重合壁袄,在計(jì)算arrowView或loadingView的center的時(shí)候类早,需要獲取stateLabel和lastUpdatedTimeLabel兩個(gè)控件的寬度并比較大小,將較大的一個(gè)作為兩個(gè)label的‘最寬距離’然想,再計(jì)算center莺奔,這樣一來(lái)就不會(huì)重合了。
而對(duì)于如何計(jì)算寬度变泄,作者給出了一個(gè)方案令哟,大家可以在以后的實(shí)踐中使用:
- (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// 根據(jù)狀態(tài)更新arrowView和loadingView的顯示if(state == MJRefreshStateIdle) {//1. 設(shè)置為默認(rèn)狀態(tài)if(oldState == MJRefreshStateRefreshing) {//1.1 從正在刷新?tīng)顟B(tài)中切換過(guò)來(lái)self.arrowView.transform =CGAffineTransformIdentity;? ? ? ? ? ? [UIViewanimateWithDuration:MJRefreshSlowAnimationDuration animations:^{//隱藏菊花self.loadingView.alpha =0.0;? ? ? ? ? ? } completion:^(BOOLfinished) {// 如果執(zhí)行完動(dòng)畫(huà)發(fā)現(xiàn)不是idle狀態(tài),就直接返回妨蛹,進(jìn)入其他狀態(tài)if(self.state != MJRefreshStateIdle)return;//菊花停止旋轉(zhuǎn)self.loadingView.alpha =1.0;? ? ? ? ? ? ? ? [self.loadingView stopAnimating];//顯示箭頭self.arrowView.hidden =NO;? ? ? ? ? ? }];? ? ? ? }else{//1.2 從其他狀態(tài)中切換過(guò)來(lái)[self.loadingView stopAnimating];//顯示箭頭并設(shè)置為初始狀態(tài)self.arrowView.hidden =NO;? ? ? ? ? ? [UIViewanimateWithDuration:MJRefreshFastAnimationDuration animations:^{self.arrowView.transform =CGAffineTransformIdentity;? ? ? ? ? ? }];? ? ? ? }? ? }elseif(state == MJRefreshStatePulling) {//2. 設(shè)置為可以刷新?tīng)顟B(tài)[self.loadingView stopAnimating];self.arrowView.hidden =NO;? ? ? ? [UIViewanimateWithDuration:MJRefreshFastAnimationDuration animations:^{//箭頭倒立self.arrowView.transform =CGAffineTransformMakeRotation(0.000001- M_PI);? ? ? ? }];? ? }elseif(state == MJRefreshStateRefreshing) {//3. 設(shè)置為正在刷新?tīng)顟B(tài)self.loadingView.alpha =1.0;// 防止refreshing -> idle的動(dòng)畫(huà)完畢動(dòng)作沒(méi)有被執(zhí)行//菊花旋轉(zhuǎn)[self.loadingView startAnimating];//隱藏arrowViewself.arrowView.hidden =YES;? ? }}
到此為止屏富,我們已經(jīng)從MJRefreshComponent到MJRefreshNormalHeader的實(shí)現(xiàn)過(guò)程看了一遍⊥苈保可以看出狠半,作者將prepare,placeSubviews以及setState:方法作為基類(lèi)的方法噩死,讓下面的子類(lèi)去一層一層實(shí)現(xiàn)。
而每一層的子類(lèi)神年,根據(jù)自身的職責(zé)已维,分別按照自己的方式來(lái)實(shí)現(xiàn)這三個(gè)方法:
MJRefreshHeader: 負(fù)責(zé)header的高度和調(diào)整header自身在外部的位置。
MJRefreshStateHeader:負(fù)責(zé)header內(nèi)部的stateLabel和lastUpdatedTimeLabel的布局和不同狀態(tài)下內(nèi)部文字的顯示已日。
MJRefreshNormalHeader:負(fù)責(zé)header內(nèi)部的loadingView以及arrowView的布局和不同狀態(tài)下的顯示垛耳。
這樣做的好處是,如果想要增加某種類(lèi)型的header飘千,只要在某一層上做文章即可堂鲜。例如該框架里的MJRefreshGifHeader,它和MJRefreshNormalHeader屬于同一級(jí),都是繼承于MJRefreshStateHeader护奈。因?yàn)槎叨季哂邢嗤问降膕tateLabel和lastUpdatedTimeLabel缔莲,唯一不同的就是左側(cè)的部分:
MJRefreshNormalHeader的左側(cè)是箭頭。
MJRefreshGifHeader的左側(cè)則是一個(gè)gif動(dòng)畫(huà)霉旗。
還是提供一張圖來(lái)直觀感受一下:
normalHeader 與 gifHeader
下面我們來(lái)看一下的實(shí)現(xiàn):
MJRefreshGifHeader
它提供了兩個(gè)接口痴奏,是用來(lái)設(shè)置不同狀態(tài)下使用的圖片數(shù)組的:
- (void)setImages:(NSArray *)images duration:(NSTimeInterval)durationforState:(MJRefreshState)state{? ? if (images == nil) return;? ? //設(shè)置不同狀態(tài)下的圖片組和持續(xù)時(shí)間self.stateImages[@(state)] = images;self.stateDurations[@(state)] = @(duration);? ? /* 根據(jù)圖片設(shè)置控件的高度 */? ? UIImage *image = [images firstObject];? ? if (image.size.height >self.mj_h) {self.mj_h = image.size.height;? ? } }- (void)setImages:(NSArray *)imagesforState:(MJRefreshState)state{? ? //如果沒(méi)有傳入duration,則根據(jù)圖片的多少來(lái)計(jì)算? ? [selfsetImages:images duration:images.count *0.1forState:state]; }
有哪些職能奖慌?
然后抛虫,和MJRefreshNormalHeader一樣,它也重寫(xiě)了基類(lèi)提供的三個(gè)方法來(lái)實(shí)現(xiàn)顯示gif圖片的職能简僧。
職能如何實(shí)現(xiàn)建椰?
1. 初始化和label的間距
- (void)prepare{? ? [superprepare];// 初始化間距self.labelLeftInset =20;}
2. 根據(jù)label的寬度和存在與否設(shè)置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都在隱藏狀態(tài)岛马,將gif劇中顯示self.gifView.contentMode = UIViewContentModeCenter;? ? } else {? ? ? ? //如果stateLabel和lastUpdatedTimeLabel中至少一個(gè)存在棉姐,則根據(jù)label的寬度設(shè)置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. 根據(jù)傳入狀態(tài)的不同來(lái)設(shè)置動(dòng)畫(huà)
- (void)setState:(MJRefreshState)state{? ? MJRefreshCheckState? ? if (state== MJRefreshStatePulling ||state== MJRefreshStateRefreshing) {? ? ? ? //1. 如果傳進(jìn)來(lái)的狀態(tài)是可以刷新和正在刷新? ? ? ? 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.如果傳進(jìn)來(lái)的狀態(tài)是默認(rèn)狀態(tài)? ? ? ? [self.gifView stopAnimating];? ? }}
Footer類(lèi)是用來(lái)處理上拉加載的,實(shí)現(xiàn)原理和下拉刷新很類(lèi)似啦逆,在這里先不介紹了~
總的來(lái)說(shuō)伞矩,該框架設(shè)計(jì)得非常工整:通過(guò)一個(gè)基類(lèi)來(lái)定義一些狀態(tài)和一些需要子類(lèi)實(shí)現(xiàn)的接口。通過(guò)一層一層地繼承,讓每一層的子類(lèi)各司其職,只完成真正屬于自己的任務(wù)荣恐,提高了框架的可定制性,而且對(duì)于功能的擴(kuò)展和bug的追蹤也很有幫助湿诊,非常值得我們參考與借鑒。
本篇文章已經(jīng)同步到我個(gè)人博客:J_Knight MJRefresh 源碼解析
歡迎來(lái)參觀 ^^
本文已在版權(quán)印備案瘦材,如需轉(zhuǎn)載請(qǐng)?jiān)L問(wèn)版權(quán)印厅须。48422928