MJRefresh 下拉刷新第三方庫(kù),是一個(gè)功能強(qiáng)大簡(jiǎn)單實(shí)用的下拉刷新控件。
整個(gè)框架邏輯清晰违崇,類之間的解耦做的很好,可定制性很高诊霹,是一款很有價(jià)值的學(xué)習(xí)框架羞延。
本文基本梳理了 header 詳細(xì)繼承關(guān)系,重點(diǎn)解釋了header 和 UIScrollView 的調(diào)用關(guān)系脾还。
文章結(jié)構(gòu)如下:
- 總體思路描述
- 每個(gè) header 類的具體功能介紹
- 小結(jié)
- header 與 UIScrollView 的關(guān)系
- 實(shí)用的知識(shí)點(diǎn)
1伴箩、總體的思路描述:
header 和 footer 本質(zhì)上都是添加到 UIScroollView 上的子控件。
在 MJRefresh 里面主要使用了 KVO 鄙漏、runtime 嗤谚、繼承、GCD 等知識(shí)點(diǎn)
MJRefreshComponent 是刷新控件的基類怔蚌,在 MJRefreshComponent 里面添加了 KVO 監(jiān)聽(tīng)巩步,prepare 方法和 placeSubview 方法。
當(dāng) MJRefreshComponent 中 KVO 監(jiān)聽(tīng)到了以后桦踊,會(huì)直接調(diào)用在 MJRefreshHeader 和 MJRefreshFooter 里實(shí)現(xiàn)的方法椅野。這里的實(shí)現(xiàn)方法里面,有主要是通過(guò)設(shè)置 state 狀態(tài)籍胯。在他們的子類里面又會(huì)分別調(diào)用 setState 方法竟闪。根據(jù)不同的狀態(tài)進(jìn)行不同的樣式變化。
上面描述一下子看起來(lái)杖狼,太過(guò)抽象不好懂炼蛤,不要急下面會(huì)有詳細(xì)介紹。
這里只需要記住本刽,MJRefresh 每個(gè)類里面最重要的機(jī)制就是:通過(guò)監(jiān)聽(tīng)的方式鲸湃,改變刷新?tīng)顟B(tài),根據(jù)狀態(tài)改變子寓,進(jìn)行相關(guān)處理暗挑。
重點(diǎn)是:監(jiān)聽(tīng)機(jī)制和狀態(tài)的改變。
抓住這兩點(diǎn)斜友。整個(gè)框架的理解就抓住了重點(diǎn)炸裆。
下面簡(jiǎn)要看一看整個(gè)類的繼承結(jié)構(gòu):
2、每個(gè)類的具體功能介紹
MJRefreshComponent (基類)
有哪些功能:
1鲜屏、聲明所有的狀態(tài) (枚舉)
2烹看、聲明控件的回調(diào)函數(shù) (NSBlock)
3国拇、添加監(jiān)聽(tīng) (主要使用KVO監(jiān)聽(tīng),scrollView 的contentOffset 和 contentSize的變化惯殊,手勢(shì)識(shí)別的 UIPanGestureRecognizer(拖動(dòng)) )
4酱吝、提供開(kāi)始刷新,停止刷新接口
5土思、聲明子類需要實(shí)現(xiàn)的方法务热。 (在基類中有部分實(shí)現(xiàn)。大部分在子類中實(shí)現(xiàn))
具體實(shí)現(xiàn)代碼:
1己儒、聲明所有的狀態(tài) (枚舉)
/** 刷新控件的狀態(tài) */
typedef NS_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
};
一共是5種狀態(tài),其中前面三種狀態(tài):閑置闪湾、松開(kāi)可刷新冲甘、正在刷新,是所有的類里面實(shí)用最多的條件判斷途样,屬于下拉刷新的狀態(tài)江醇。
即將刷新?tīng)顟B(tài),只是拿來(lái)預(yù)防view還沒(méi)顯示出來(lái)就調(diào)用了beginRefreshing娘纷,不用關(guān)注嫁审。
第五種狀態(tài)跋炕,沒(méi)有更多數(shù)據(jù)的狀態(tài)是屬于上拉刷新的狀態(tài)赖晶。
2、聲明控件的回調(diào)函數(shù) (NSBlock)
/** 進(jìn)入刷新?tīng)顟B(tài)的回調(diào) */
typedef void (^MJRefreshComponentRefreshingBlock)();
/** 開(kāi)始刷新后的回調(diào)(進(jìn)入刷新?tīng)顟B(tài)后的回調(diào)) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)();
/** 結(jié)束刷新后的回調(diào) */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)();
3辐烂、添加監(jiān)聽(tīng) (主要使用KVO監(jiān)聽(tīng)遏插,scrollView 的contentOffset 和 contentSize的變化,手勢(shì)識(shí)別的 UIPanGestureRecognizer(拖動(dòng)) )
監(jiān)聽(tīng)的聲明:
- (void)addObservers
{
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];//contentOffset屬性
[self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];//contentSize屬性
self.pan = self.scrollView.panGestureRecognizer;
[self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];//UIPanGestureRecognizer 的state屬性
}
監(jiān)聽(tīng)的處理:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// 遇到這些情況就直接返回
if (!self.userInteractionEnabled) return;
// 這個(gè)就算看不見(jiàn)也需要處理
if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
[self scrollViewContentSizeDidChange:change];
}
// 看不見(jiàn)
if (self.hidden) return;
if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
[self scrollViewContentOffsetDidChange:change];
} else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
[self scrollViewPanStateDidChange:change];
}
}
其實(shí)纠修,此處的監(jiān)聽(tīng)處理胳嘲,只不過(guò)是用來(lái)連接基類和子類的方法實(shí)現(xiàn)。
這個(gè)三個(gè)方法都是在子類里面實(shí)現(xiàn)的:
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
在后面子類的分析里扣草,會(huì)看到這些實(shí)現(xiàn)的類到底做了什么了牛。
4、提供開(kāi)始刷新辰妙,停止刷新接口
#pragma mark 進(jìn)入刷新?tīng)顟B(tài)
- (void)beginRefreshingWithCompletionBlock:(void (^)())completionBlock
{
self.beginRefreshingCompletionBlock = completionBlock;
[self beginRefreshing];
}
- (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)要重新刷新一下)
[self setNeedsDisplay];
}
}
}
#pragma mark 結(jié)束刷新?tīng)顟B(tài)
- (void)endRefreshing
{
self.state = MJRefreshStateIdle;
}
- (void)endRefreshingWithCompletionBlock:(void (^)())completionBlock
{
self.endRefreshingCompletionBlock = completionBlock;
[self endRefreshing];
}
#pragma mark 是否正在刷新
- (BOOL)isRefreshing
{
return self.state == MJRefreshStateRefreshing || self.state == MJRefreshStateWillRefresh;
}
上面主要是一些狀態(tài)的切換蛙婴。
5、聲明子類需要實(shí)現(xiàn)的方法尔破。 (在基類中有部分實(shí)現(xiàn)街图。大部分在子類中實(shí)現(xiàn))
#pragma mark - 交給子類們?nèi)?shí)現(xiàn)
/** 初始化 */
- (void)prepare NS_REQUIRES_SUPER;
/** 擺放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的contentOffset發(fā)生改變的時(shí)候調(diào)用 */
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的contentSize發(fā)生改變的時(shí)候調(diào)用 */
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的拖拽狀態(tài)發(fā)生改變的時(shí)候調(diào)用 */
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
MJRefreshHeader (繼承自 MJRefreshComponet)
主要功能:
1浇衬、初始化類方法 (主要是生成header對(duì)象和傳入的block)
2、設(shè)置 header 的高度餐济。
3耘擂、重新調(diào)整 header 的 Y 值。 (其實(shí)絮姆,header本身是UIScrollView的子視圖)
4梳星、實(shí)現(xiàn)方法:scrollViewContentOffsetDidChange:
(根據(jù) contentOffset 的變化,來(lái)切換狀態(tài)滚朵,也就是基類里面定義的幾個(gè)狀態(tài))
5冤灾、實(shí)現(xiàn) setState 方法 (依據(jù)不同的狀態(tài),出現(xiàn)不同的動(dòng)畫(huà)辕近。)
具體實(shí)現(xiàn):
1韵吨、初始化類方法 (主要是生成header對(duì)象和傳入的block)
初始化有兩種方法:
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
MJRefreshHeader *cmp = [[self alloc] init];
//傳入block
cmp.refreshingBlock = refreshingBlock;
return cmp;
}
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
MJRefreshHeader *cmp = [[self alloc] init];
//設(shè)置self.refreshingTarget 和 self.refreshingAction
[cmp setRefreshingTarget:target refreshingAction:action];
return cmp;
}
以上方法,是在生成一個(gè) header 最常調(diào)用的方法移宅。
2归粉、設(shè)置 header 的高度。
通過(guò)重新父類的 prepare 方法漏峰,來(lái)設(shè)置 header 的高度糠悼。
- (void)prepare
{
[super prepare];//必須調(diào)用父類方法
// 設(shè)置用于在NSUserDefaults里存儲(chǔ)時(shí)間的key
self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
// 設(shè)置header的高度
self.mj_h = MJRefreshHeaderHeight;
}
3、重新調(diào)整 header 的 Y 值浅乔。 (其實(shí)倔喂,header本身是UIScrollView的子視圖)
重寫(xiě) placeSubView 方法,重新調(diào)整 Y 值靖苇。
- (void)placeSubviews
{
[super placeSubviews];
// 設(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悼枢、實(shí)現(xiàn)方法:scrollViewContentOffsetDidChange:
(根據(jù) contentOffset 的變化,來(lái)切換狀態(tài)脾拆,也就是基類里面定義的幾個(gè)狀態(tài))
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
// 這個(gè)方法在父類里面有實(shí)現(xiàn)馒索,但是什么也沒(méi)有做。因?yàn)樯厦嬲f(shuō)過(guò)名船,具體的實(shí)現(xiàn)都是在子類里面做的處理
[super scrollViewContentOffsetDidChange: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è)
CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
//118
insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
//設(shè)置contentInset
self.scrollView.mj_insetT = insetT;
// 記錄刷新的時(shí)候的偏移量 -54 = 64 - 118
self.insetTDelta = _scrollViewOriginalInset.top - insetT;
return;
}
// 跳轉(zhuǎn)到下一個(gè)控制器時(shí)包帚,contentInset可能會(huì)變
_scrollViewOriginalInset = self.scrollView.contentInset;
// 記錄當(dāng)前的contentOffset
CGFloat offsetY = self.scrollView.mj_offsetY;
// 頭部控件剛好全部出現(xiàn)的offsetY,默認(rèn)是-64(20 + 44)
CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
// 向上滾動(dòng)渔期,直接返回
if (offsetY > happenOffsetY) return;
// 從普通 到 即將刷新 的臨界距離
CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;// -64 - 54 = -118
//下拉的百分比:下拉的距離與header高度的比值
CGFloat pullingPercent = (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;
} else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
// 如果當(dāng)前狀態(tài)為可以刷新 && 下拉的距離小于臨界距離,則將狀態(tài)切換為默認(rèn)
self.state = MJRefreshStateIdle;
}
} else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開(kāi)
// 手松開(kāi) && 狀態(tài)為可以刷新(MJRefreshStatePulling)時(shí) 開(kāi)始刷新
[self beginRefreshing];
} else if (pullingPercent < 1) {
//手松開(kāi)后疯趟,默認(rèn)狀態(tài)時(shí)拘哨,恢復(fù)self.pullingPercent
self.pullingPercent = pullingPercent;
}
}
這個(gè)方法,應(yīng)該是所有子類方法里面最復(fù)雜的方法實(shí)現(xiàn)信峻,因?yàn)檫@里要考慮倦青,三種刷新?tīng)顟B(tài)的臨界點(diǎn)切換。
需要注意三點(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)切換為正在刷新。
三個(gè)狀態(tài)圖:
5鳍征、實(shí)現(xiàn) setState 方法 (依據(jù)不同的狀態(tài)黍翎,出現(xiàn)不同的動(dòng)畫(huà)。)
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
if (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í)間
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
[[NSUserDefaults standardUserDefaults] synchronize];
// 恢復(fù)inset和offset
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
//118 -> 64(剪去了header的高度)
self.scrollView.mj_insetT += self.insetTDelta;
// 自動(dòng)調(diào)整透明度
if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
} completion:^(BOOL finished) {
self.pullingPercent = 0.0;
if (self.endRefreshingCompletionBlock) {
//調(diào)用刷新完成的block
self.endRefreshingCompletionBlock();
}
}];
} else if (state == MJRefreshStateRefreshing) {
//============== 設(shè)置狀態(tài)為正在刷新?tīng)顟B(tài) =============//
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;//64 + 54 (都是默認(rèn)的高度)
// 重新設(shè)置contentInset,top = 118
self.scrollView.mj_insetT = top;
// 設(shè)置滾動(dòng)位置
[self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
} completion:^(BOOL finished) {
//調(diào)用進(jìn)行刷新的block
[self executeRefreshingCallback];
}];
});
}
}
這里需要注意兩點(diǎn):
這里狀態(tài)的切換质礼,主要圍繞著兩種:默認(rèn)狀態(tài)和正在刷新態(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 (繼承自 MJRefreshHeader)
主要功能:
1造挽、簡(jiǎn)單布局 stateLable(狀態(tài)labe) 和 lastUpdatedTimeLabel(時(shí)間 label)
2、根據(jù)刷新?tīng)顟B(tài)的變化弄痹,設(shè)置兩個(gè) label 的文本饭入。
給一張圖,讓大家直觀感受一下這兩個(gè) label 控件:
功能實(shí)現(xiàn):
1肛真、簡(jiǎn)單布局 stateLable(狀態(tài)labe) 和 lastUpdatedTimeLabel(時(shí)間 label)
還是重新父類的 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];
}
在這里谐丢,將每一個(gè)狀態(tài)對(duì)應(yīng)的提示文字放入一個(gè)字典里面,key是狀態(tài)的NSNumber形式.
- (void)setTitle:(NSString *)title forState:(MJRefreshState)state
{
if (title == nil) return;
self.stateTitles[@(state)] = title;
self.stateLabel.text = self.stateTitles[@(self.state)];
}
依據(jù)時(shí)間 label 是否隱藏,對(duì)兩個(gè) label 進(jìn)行布局:
- (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(高度各占一半)
CGFloat stateLabelH = 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í)間label
if (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;
}
}
}
2讥珍、根據(jù)刷新?tīng)顟B(tài)的變化,設(shè)置兩個(gè) label 的文本窄瘟。
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 設(shè)置狀態(tài)文字
self.stateLabel.text = self.stateTitles[@(state)];
// 重新設(shè)置key(重新顯示時(shí)間)
self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}
這里傳入的狀態(tài)不同衷佃,在 stateLabel 和 lastUpdatedLabel 里面文字也不同。
stateLabel 可以直接從字典 stateTitles 里面取到 蹄葱。
lastUpdatedLabel 需要使用下面的方法:
- (void)setLastUpdatedTimeKey:(NSString *)lastUpdatedTimeKey
{
[super setLastUpdatedTimeKey:lastUpdatedTimeKey];
// 如果label隱藏了氏义,就不用再處理
if (self.lastUpdatedTimeLabel.hidden) return;
//根據(jù)key,從NSUserDefaults獲取對(duì)應(yīng)的NSData型時(shí)間
NSDate *lastUpdatedTime = [[NSUserDefaults standardUserDefaults] 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 = [self currentCalendar];
NSUInteger unitFlags = NSCalendarUnitYear| NSCalendarUnitMonth | NSCalendarUnitDay |NSCalendarUnitHour |NSCalendarUnitMinute;
NSDateComponents *cmp1 = [calendar components:unitFlags fromDate:lastUpdatedTime];
NSDateComponents *cmp2 = [calendar components:unitFlags fromDate:[NSDate date]];
// 2.格式化日期
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
BOOL isToday = NO;
if ([cmp1 day] == [cmp2 day]) {
//今天竣况,省去年月日
formatter.dateFormat = @" HH:mm";
isToday = YES;
} else if ([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 = [NSString stringWithFormat:@"%@%@%@",
[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"",
time];
} else {
// 沒(méi)有獲得上次更新時(shí)間(應(yīng)該是第一次更新或者多次更新鸠补,之前的更新都失敗了)
self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@",
[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
[NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];
}
}
此處借鑒一下,時(shí)間比較的處理嘀掸。使用 NSDateComponents 類進(jìn)行時(shí)間比較紫岩。
MJRefreshNomalHeader (繼承自 MJRefreshStarteHeader )
主要功能:
1、添加箭頭和小菊花控件
2睬塌、布局這兩個(gè)控件泉蝌,并且在 refresh 控件的狀態(tài)切換的時(shí)候,改變這兩個(gè) view 的樣式揩晴。
直觀認(rèn)識(shí)一下這兩個(gè)控件:
功能實(shí)現(xiàn):(同樣是實(shí)現(xiàn)了父類的三個(gè)方法)
1勋陪、prepare 方法,設(shè)置小菊花樣式硫兰。
- (void)prepare
{
[super prepare];
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) {
CGFloat stateWidth = 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;
}
這里需要注意就是箭頭或者小菊花,是處于 stateLabel 和
lastUpdateTimeLabel 左邊的泳赋。在計(jì)算arrowView或loadingView的
center的時(shí)候雌桑,需要獲取stateLabel和lastUpdatedTimeLabel兩個(gè)控件的
寬度并比較大小,將較大的一個(gè)作為兩個(gè)label的‘最寬距離’祖今,再計(jì)算center校坑,
這樣一來(lái)就不會(huì)重合了拣技。
關(guān)于計(jì)算寬度的方法,大家可以在以后的實(shí)踐中使用:
- (CGFloat)mj_textWith {
CGFloat stringWidth = 0;
CGSize size = CGSizeMake(MAXFLOAT, MAXFLOAT);
if (self.text.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
stringWidth =[self.text
boundingRectWithSize:size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName:self.font}
context:nil].size.width;
#else
stringWidth = [self.text sizeWithFont:self.font
constrainedToSize:size
lineBreakMode:NSLineBreakByCharWrapping].width;
#endif
}
return stringWidth;
}
3耍目、 setState 方法實(shí)現(xiàn)过咬。與父類的同名方法一樣,這里同樣是判斷狀態(tài)的變化制妄,處理相關(guān)事件掸绞。這里處理的是箭頭和小菊花的動(dòng)畫(huà)。
- (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;
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
//隱藏菊花
self.loadingView.alpha = 0.0;
} completion:^(BOOL finished) {
// 如果執(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;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowView.transform = CGAffineTransformIdentity;
}];
}
} else if (state == MJRefreshStatePulling) {
//2. 設(shè)置為可以刷新?tīng)顟B(tài)
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
//箭頭倒立
self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
}];
} else if (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];
//隱藏arrowView
self.arrowView.hidden = YES;
}
}
3、小結(jié)
以上俺抽,我們已經(jīng)從基類 MJrefreshComponent 到 MJRefreshNormalHeader 敞映,完整地梳理了繼承的方法實(shí)現(xiàn)和各自的功能代碼。
可以清晰的看到磷斧,每個(gè)子類都實(shí)現(xiàn)了父類的 prepare振愿、placeView、setState 方法弛饭。每一個(gè)子類分工明確冕末,結(jié)構(gòu)清晰。
- MJRefreshHeader: 處理了 header 的高度侣颂,并且調(diào)整了 header 在 scrollView 里面的位置档桃。
- MJRefreshStateHeader:處理了 header 內(nèi)部?jī)蓚€(gè)子控件 stateLabel 和 lastUpdateeTimeLabel 的布局和不同狀態(tài)的不同文字顯示。
- MJRefreshNormalHeader:處理了 header 內(nèi)部的另外兩個(gè)子控件憔晒,小箭頭和小菊花的布局藻肄,以及不同狀態(tài)的顯示。
以上的結(jié)構(gòu)拒担,把一個(gè)看起來(lái)簡(jiǎn)單的 header 分成四個(gè)類來(lái)寫(xiě)嘹屯。很好的把子控件之間耦合度降低,便于我們進(jìn)行定制化處理从撼。
例如該框架里的MJRefreshGifHeader,它和MJRefreshNormalHeader屬于同一級(jí)州弟,都是繼承于MJRefreshStateHeader。因?yàn)槎叨季哂邢嗤问降膕tateLabel和lastUpdatedTimeLabel谋逻,唯一不同的就是左側(cè)的部分:
MJRefreshNormalHeader的左側(cè)是箭頭呆馁。
MJRefreshGifHeader的左側(cè)則是一個(gè)gif動(dòng)畫(huà)。
相關(guān)代碼解析毁兆,此處就不一一說(shuō)明。
可以參考:MJRefresh源碼解析主要思路
4阴挣、header 與 UIScrollView 的關(guān)系
上面的三大步驟,其實(shí)都是在講一個(gè) header 的構(gòu)成。
那我們茎芭,平常在給 UITableView 或者 UICollectionView 添加 header 的過(guò)程到底是怎么實(shí)現(xiàn)的呢揖膜?
下面就帶大家看一看,header 到底是怎么添加到父視圖 UIScrollView 上的梅桩。
此處壹粟,有個(gè)問(wèn)題要清楚:header 為什么是添加在 UIScrollView 上的呢?
答:**UITableView 和 UICollectionView 都是繼承自 UIScrollView **宿百。
接下來(lái)又有問(wèn)題了:系統(tǒng)自帶的 UIScrollView 怎么可以添加子視圖 header 呢趁仙?
答:使用 runtime 的關(guān)聯(lián)屬性+category,可以給UIScrollView 添加兩個(gè)屬性垦页,header 和 footer 雀费。(運(yùn)行時(shí)只是參加我的Objective-C 的 runtime 特性與小蝌蚪找媽媽
)
以上,兩個(gè)問(wèn)題明白了痊焊,就可以去研究 MJRefresh 框架里面的 UIScrollView+MJRefresh類盏袄。
關(guān)聯(lián)屬性方法:
- (void)setMj_header:(MJRefreshHeader *)mj_header
{
if (mj_header != self.mj_header) {
// 刪除舊的,添加新的
[self.mj_header removeFromSuperview];
[self insertSubview:mj_header atIndex:0];
// 存儲(chǔ)新的
[self willChangeValueForKey:@"mj_header"]; // KVO
objc_setAssociatedObject(self, &MJRefreshHeaderKey,
mj_header, OBJC_ASSOCIATION_ASSIGN);
[self didChangeValueForKey:@"mj_header"]; // KVO
}
}
- (MJRefreshHeader *)mj_header
{
return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}
這里把 header 添加到 UIScrollView 里面使用的是 UIView 的方法:
[self insertSubview:mj_header atIndex:0];
footer 的添加也是同理薄啥。
到這里辕羽,可以知道利用關(guān)聯(lián)屬性,可以把 header 添加到 UIScrollView 上面垄惧。
但大家有沒(méi)有想過(guò)一個(gè)問(wèn)題:
header 是 UIScrollView 的子控件逛漫,header 控件是怎樣對(duì) UIScrollView 父控件添加 KVO 的監(jiān)聽(tīng)的呢?
這一點(diǎn)赘艳,至關(guān)重要酌毡,如果子控件 header 無(wú)法給父控件添加監(jiān)聽(tīng)事件,那么蕾管,header 就不會(huì)響應(yīng)狀態(tài)的變化枷踏。
下面來(lái)看看,MJRefresh 框架是怎么做的:
- 在 header 的基類里面聲明了一個(gè)成員變量:
/** 刷新控件的基類 */
@interface MJRefreshComponent : UIView
{
/** 記錄scrollView剛開(kāi)始的inset */
UIEdgeInsets _scrollViewOriginalInset;
/** 父控件 */
__weak UIScrollView *_scrollView;
}
- 緊接著 MJRefreshComponent.m 文件里實(shí)現(xiàn)了方法 willMoveToSuperview :
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 如果不是UIScrollView掰曾,不做任何事情
if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
// 舊的父控件移除監(jiān)聽(tīng)
[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最開(kāi)始的contentInset
_scrollViewOriginalInset = _scrollView.contentInset;
// 添加監(jiān)聽(tīng)
[self addObservers];
}
}
willMoveToSuperview 是 UIView 的方法旭蠕,在當(dāng)前的 UIView 被添加到父類的時(shí)候就會(huì)調(diào)用。
還記得上面的方法:[self insertSubview:mj_header atIndex:0]旷坦?
這句話掏熬,把 header 實(shí)例對(duì)象添加到了 UIScrollView 里面。這個(gè)方法之后秒梅,會(huì)立馬調(diào)用 willMoveToSuperview 方法旗芬。
在 willMoveToSuperview 方法里面,
// 記錄UIScrollView
_scrollView = (UIScrollView *)newSuperview;
// 設(shè)置永遠(yuǎn)支持垂直彈簧效果
_scrollView.alwaysBounceVertical = YES;
// 記錄UIScrollView最開(kāi)始的contentInset
_scrollViewOriginalInset = _scrollView.contentInset;
// 添加監(jiān)聽(tīng)
[self addObservers];
這樣捆蜀,就實(shí)現(xiàn)了疮丛,子控件 header 對(duì)于父控件 UIScrollView 的弱引用幔嫂,
同時(shí),添加了監(jiān)聽(tīng)方法誊薄。
//MJRefreshComponent.m
#pragma mark - KVO監(jiān)聽(tīng)
- (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];
}
經(jīng)過(guò)上面的分析履恩,應(yīng)該清楚的是:willMoveToSuperview 方法,連接了 UIScrollView 和動(dòng)態(tài)添加的 header 的調(diào)用關(guān)系呢蔫。
5切心、實(shí)用的知識(shí)點(diǎn)
1、關(guān)鍵字 NS_REQUIRES_SUPER
這個(gè)關(guān)鍵字 NS_REQUIRES_SUPER 放在方法聲明的后面片吊,意思是說(shuō)绽昏,子類在實(shí)現(xiàn)父類的這個(gè)方法的時(shí)候,必須先調(diào)用父類方法定鸟。
比如:在 MJRefreshComponent.h 里面
/** 初始化 */
- (void)prepare NS_REQUIRES_SUPER;
/** 擺放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的contentOffset發(fā)生改變的時(shí)候調(diào)用 */
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的contentSize發(fā)生改變的時(shí)候調(diào)用 */
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的拖拽狀態(tài)發(fā)生改變的時(shí)候調(diào)用 */
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
2而涉、關(guān)聯(lián)屬性的應(yīng)用
此處略過(guò)。放上 Objective-C 的 runtime 特性與小蝌蚪找媽媽
联予。
3啼县、計(jì)算 UILabel 寬度的方法
- (CGFloat)mj_textWith {
CGFloat stringWidth = 0;
CGSize size = CGSizeMake(MAXFLOAT, MAXFLOAT);
if (self.text.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
stringWidth =[self.text
boundingRectWithSize:size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName:self.font}
context:nil].size.width;
#else
stringWidth = [self.text sizeWithFont:self.font
constrainedToSize:size
lineBreakMode:NSLineBreakByCharWrapping].width;
#endif
}
return stringWidth;
}
4、架構(gòu)的思想
不得不說(shuō)沸久,MJRefresh 確實(shí)是一個(gè)優(yōu)秀的 iOS 開(kāi)源庫(kù)季眷。
結(jié)構(gòu)清晰,分工明確卷胯。完美的詮釋了編程的基本原則:單一職責(zé)原則子刮。
難怪,很多大牛都說(shuō)窑睁,編程能力的進(jìn)階需要研究開(kāi)源庫(kù)挺峡。
希望,大家有所收獲担钮。
參考文章: