簡(jiǎn)書博客已經(jīng)暫停更新,想看更多技術(shù)博客請(qǐng)到:
MJRefresh是李明杰老師的作品衷敌,到現(xiàn)在已經(jīng)有9800多顆star了荆陆,是一個(gè)簡(jiǎn)單實(shí)用吼和,功能強(qiáng)大的iOS下拉刷新(也支持上拉加載更多)控件洛口。它的可定制性很高影兽,幾乎可以滿足大部分下拉刷新的設(shè)計(jì)需求,值得學(xué)習(xí)舰始。
該框架的結(jié)構(gòu)設(shè)計(jì)得很清晰崇棠,使用一個(gè)基類MJRefreshComponent
來做一些基本的設(shè)定,然后通過繼承的方式丸卷,讓MJRefreshHeader
和MJRefreshFooter
分別具備下拉刷新和上拉加載的功能枕稀。從繼承機(jī)構(gòu)來看可以分為三層,具體可以從下面的圖里看出來:
首先來看一下該控件的基類:MJRefreshComponent:
MJRefreshComponent
這個(gè)類作為該控件的基類谜嫉,涵蓋了基類所具備的一些:狀態(tài)萎坷,回調(diào)block等,大致分成下面這5種職能:
有哪些職能?
- 聲明控件的所有狀態(tài)沐兰。
- 聲明控件的回調(diào)函數(shù)哆档。
- 添加監(jiān)聽。
- 提供刷新住闯,停止刷新接口瓜浸。
- 提供子類需要實(shí)現(xiàn)的方法。
職能如何實(shí)現(xiàn)比原?
1. 聲明控件的所有狀態(tài)
/** 刷新控件的狀態(tài) */
typedef NS_ENUM(NSInteger, MJRefreshState) {
/** 普通閑置狀態(tài) */
MJRefreshStateIdle = 1,
/** 松開就可以進(jìn)行刷新的狀態(tài) */
MJRefreshStatePulling,
/** 正在刷新中的狀態(tài) */
MJRefreshStateRefreshing,
/** 即將刷新的狀態(tài) */
MJRefreshStateWillRefresh,
/** 所有數(shù)據(jù)加載完畢插佛,沒有更多的數(shù)據(jù)了 */
MJRefreshStateNoMoreData
};
2. 聲明控件的回調(diào)函數(shù)
/** 進(jìn)入刷新狀態(tài)的回調(diào) */
typedef void (^MJRefreshComponentRefreshingBlock)();
/** 開始刷新后的回調(diào)(進(jìn)入刷新狀態(tài)后的回調(diào)) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)();
/** 結(jié)束刷新后的回調(diào) */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)();
3. 添加監(jiān)聽
監(jiān)聽的聲明:
- (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屬性
}
對(duì)于監(jiān)聽的處理:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// 遇到這些情況就直接返回
if (!self.userInteractionEnabled) return;
// 這個(gè)就算看不見也需要處理
if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
[self scrollViewContentSizeDidChange:change];
}
// 看不見
if (self.hidden) return;
if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
[self scrollViewContentOffsetDidChange:change];
} else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
[self scrollViewPanStateDidChange:change];
}
}
4. 提供刷新,停止刷新接口
#pragma mark 進(jìn)入刷新狀態(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è)控制器的情況,回來要重新刷新一下)
[self setNeedsDisplay];
}
}
}
#pragma mark 結(jié)束刷新狀態(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;
}
交給子類實(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;
5. 提供子類需要實(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;
從上面等結(jié)構(gòu)圖可以看出蚌铜,緊接著這個(gè)基類谢床,下面分為MJRefreshHeader
和MJRefreshFooter
,這里順著MJRefreshHeader
這個(gè)分支向下展開:
MJRefreshHeader
MJRefreshHeader
繼承于MJRefreshComponent
,它做了這幾件事:
有哪些職能厘线?
- 初始化识腿。
- 設(shè)置header高度。
- 重新調(diào)整y值造壮。
- 根據(jù)
contentOffset
的變化渡讼,來切換狀態(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 = [[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;
}
2. 設(shè)置header高度
通過重寫prepare
方法來設(shè)置header的高度:
- (void)prepare
{
[super prepare];
// 設(shè)置用于在NSUserDefaults里存儲(chǔ)時(shí)間的key
self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
// 設(shè)置header的高度
self.mj_h = MJRefreshHeaderHeight;
}
3. 重新調(diào)整y值
通過重寫placeSubviews
方法來重新調(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. 狀態(tài)切換的代碼:
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[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) {// 即將刷新 && 手松開
// 手松開 && 狀態(tài)為可以刷新(MJRefreshStatePulling)時(shí) 開始刷新
[self beginRefreshing];
} else if (pullingPercent < 1) {
//手松開后挤聘,默認(rèn)狀態(tài)時(shí)轰枝,恢復(fù)self.pullingPercent
self.pullingPercent = pullingPercent;
}
}
需要注意三點(diǎn):
- 這里的狀態(tài)有三種:默認(rèn)狀態(tài)(MJRefreshStateIdle),可以刷新的狀態(tài)(MJRefreshStatePulling)以及正在刷新的狀態(tài)(MJRefreshStateRefreshing)组去。
- 狀態(tài)切換的因素有兩個(gè):一個(gè)是下拉的距離是否超過臨界值鞍陨,另一個(gè)是 手指是否離開屏幕。
- 注意:可以刷新的狀態(tài)和正在刷新的狀態(tài)是不同的添怔。因?yàn)樵谑种高€貼在屏幕的時(shí)候是不能進(jìn)行刷新的湾戳。所以即使在下拉的距離超過了臨界距離(狀態(tài)欄 + 導(dǎo)航欄 + header高度)贤旷,如果手指沒有離開屏幕广料,那么也不能馬上進(jìn)行刷新,而是將狀態(tài)切換為:可以刷新幼驶。一旦手指離開了屏幕艾杏,馬上將狀態(tài)切換為正在刷新。
這里提供一張圖來體現(xiàn)三個(gè)狀態(tài)的不同:
5. 狀態(tài)切換時(shí)的相應(yīng)操作:
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
if (state == MJRefreshStateIdle) {
//============== 設(shè)置狀態(tài)為默認(rèn)狀態(tài) =============//
//如果當(dāng)前不是正在刷新就返回盅藻,因?yàn)檫@個(gè)方法主要針對(duì)從正在刷新狀態(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à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ì)開始刷新和結(jié)束刷新這兩個(gè)切換點(diǎn)。
- 從正在刷新狀態(tài)狀態(tài)切換為默認(rèn)狀態(tài)時(shí)(結(jié)束刷新)假残,需要記錄刷新結(jié)束的時(shí)間缭贡。因?yàn)閔eader里面有一個(gè)默認(rèn)的label是用來顯示上次刷新的時(shí)間的。
MJRefreshStateHeader
這個(gè)類是MJRefreshHeader
類的子類辉懒,它做了兩件事:
有哪些職能阳惹?
- 簡(jiǎn)單布局了
stateLabel
和lastUpdatedTimeLabel
。 - 根據(jù)控件狀態(tài)的切換(默認(rèn)狀態(tài)眶俩,正在刷新狀態(tài))莹汤,實(shí)現(xiàn)了這兩個(gè)label顯示的文字的切換。
給一張圖颠印,讓大家直觀感受一下這兩個(gè)控件:
職能如何實(shí)現(xiàn)纲岭?
這個(gè)類通過覆蓋父類三個(gè)方法來實(shí)現(xiàn)上述兩個(gè)實(shí)現(xiàn):
方法1: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)];
}
方法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(高度各占一半)
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;
}
}
}
這里主要是對(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
里的文字需要通過一個(gè)方法來取出即可:
- (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里拿來時(shí)間窗轩,這應(yīng)該是用戶自定義顯示時(shí)間格式的渠道
if (self.lastUpdatedTimeText) {
self.lastUpdatedTimeLabel.text = self.lastUpdatedTimeText(lastUpdatedTime);
return;
}
//如果沒有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 {
// 沒有獲得上次更新時(shí)間(應(yīng)該是第一次更新或者多次更新堤舒,之前的更新都失敗了)
self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@",
[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
[NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];
}
}
在這里注意兩點(diǎn):
- 作者通過使用block來讓用戶自己定義日期現(xiàn)實(shí)的格式,如果用戶沒有自定義哺呜,就使用作者提供的默認(rèn)格式舌缤。
- 在默認(rèn)格式的設(shè)置里,判斷了是否是今日某残,是否是今年的情況国撵。在以后設(shè)計(jì)顯示時(shí)間的labe的時(shí)候可以借鑒一下。
MJRefreshNormalHeader
有哪些職能玻墅?
MJRefreshNormalHeader 繼承于 MJRefreshStateHeader介牙,它主要做了兩件事:
- 它在MJRefreshStateHeader上添加了
_arrowView
和loadingView
。 - 布局了這兩個(gè)view并在Refresh控件的狀態(tài)切換的時(shí)候改變這兩個(gè)view的樣式澳厢。
還是給一張圖來直觀感受一下這兩個(gè)view:
職能如何實(shí)現(xiàn)环础?
同MJRefreshStateHeader一樣,也是重寫了父類的三個(gè)方法:
方法1:prepare
- (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;
}
在這里注意一點(diǎn):因?yàn)?code>stateLabel和
lastUpdatedTimeLabel
是上下并排分布的,而arrowView
或loadingView
是在這二者的左邊裸扶,所以為了避免這兩組重合框都,在計(jì)算arrowView
或loadingView
的center的時(shí)候,需要獲取stateLabel
和lastUpdatedTimeLabel
兩個(gè)控件的寬度并比較大小,將較大的一個(gè)作為兩個(gè)label的‘最寬距離’魏保,再計(jì)算center熬尺,這樣一來就不會(huì)重合了。
而對(duì)于如何計(jì)算寬度谓罗,作者給出了一個(gè)方案粱哼,大家可以在以后的實(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:
- (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ài)中切換過來
self.arrowView.transform = CGAffineTransformIdentity;
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
//隱藏菊花
self.loadingView.alpha = 0.0;
} completion:^(BOOL finished) {
// 如果執(zhí)行完動(dòng)畫發(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)中切換過來
[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à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ài)
self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動(dòng)畫完畢動(dòng)作沒有被執(zhí)行
//菊花旋轉(zhuǎn)
[self.loadingView startAnimating];
//隱藏arrowView
self.arrowView.hidden = YES;
}
}
到此為止揭措,我們已經(jīng)從MJRefreshComponent
到MJRefreshNormalHeader
的實(shí)現(xiàn)過程看了一遍】舔牵可以看出绊含,作者將prepare
,placeSubviews
以及setState:
方法作為基類的方法,讓下面的子類去一層一層實(shí)現(xiàn)炊汹。
而每一層的子類躬充,根據(jù)自身的職責(zé),分別按照自己的方式來實(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)下的顯示。
這樣做的好處是霸褒,如果想要增加某種類型的header伴找,只要在某一層上做文章即可。例如該框架里的MJRefreshGifHeader
,它和MJRefreshNormalHeader
屬于同一級(jí)傲霸,都是繼承于MJRefreshStateHeader
疆瑰。因?yàn)槎叨季哂邢嗤问降?code>stateLabel和lastUpdatedTimeLabel
眉反,唯一不同的就是左側(cè)的部分:
-
MJRefreshNormalHeader
的左側(cè)是箭頭昙啄。 -
MJRefreshGifHeader
的左側(cè)則是一個(gè)gif動(dòng)畫。
還是提供一張圖來直觀感受一下:
下面我們來看一下的實(shí)現(xiàn):
MJRefreshGifHeader
它提供了兩個(gè)接口寸五,是用來設(shè)置不同狀態(tài)下使用的圖片數(shù)組的:
- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(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 *)images forState:(MJRefreshState)state
{
//如果沒有傳入duration梳凛,則根據(jù)圖片的多少來計(jì)算
[self setImages:images duration:images.count * 0.1 forState:state];
}
有哪些職能?
然后梳杏,和MJRefreshNormalHeader
一樣韧拒,它也重寫了基類提供的三個(gè)方法來實(shí)現(xiàn)顯示gif圖片的職能。
職能如何實(shí)現(xiàn)十性?
1. 初始化和label的間距
- (void)prepare
{
[super prepare];
// 初始化間距
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;
CGFloat stateWidth = 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)的不同來設(shè)置動(dòng)畫
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {
//1. 如果傳進(jìn)來的狀態(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)來的狀態(tài)是默認(rèn)狀態(tài)
[self.gifView stopAnimating];
}
}
Footer類是用來處理上拉加載的楷掉,實(shí)現(xiàn)原理和下拉刷新很類似,在這里先不介紹了~
總的來說霞势,該框架設(shè)計(jì)得非常工整:通過一個(gè)基類來定義一些狀態(tài)和一些需要子類實(shí)現(xiàn)的接口烹植。通過一層一層地繼承斑鸦,讓每一層的子類各司其職,只完成真正屬于自己的任務(wù)草雕,提高了框架的可定制性巷屿,而且對(duì)于功能的擴(kuò)展和bug的追蹤也很有幫助,非常值得我們參考與借鑒墩虹。
本篇文章已經(jīng)同步到我個(gè)人博客:J_Knight MJRefresh 源碼解析
歡迎來參觀 ^^
本文已在版權(quán)印備案嘱巾,如需轉(zhuǎn)載請(qǐng)?jiān)L問版權(quán)印。48422928
-------------------------------- 2018年7月17日更新 --------------------------------
注意注意=氲觥E啊!
筆者在近期開通了個(gè)人公眾號(hào)尖坤,主要分享編程稳懒,讀書筆記,思考類的文章慢味。
- 編程類文章:包括筆者以前發(fā)布的精選技術(shù)文章场梆,以及后續(xù)發(fā)布的技術(shù)文章(以原創(chuàng)為主),并且逐漸脫離 iOS 的內(nèi)容纯路,將側(cè)重點(diǎn)會(huì)轉(zhuǎn)移到提高編程能力的方向上或油。
- 讀書筆記類文章:分享編程類,思考類驰唬,心理類顶岸,職場(chǎng)類書籍的讀書筆記。
- 思考類文章:分享筆者平時(shí)在技術(shù)上叫编,生活上的思考辖佣。
因?yàn)楣娞?hào)每天發(fā)布的消息數(shù)有限制,所以到目前為止還沒有將所有過去的精選文章都發(fā)布在公眾號(hào)上搓逾,后續(xù)會(huì)逐步發(fā)布的卷谈。
而且因?yàn)楦鞔蟛┛推脚_(tái)的各種限制,后面還會(huì)在公眾號(hào)上發(fā)布一些短小精干霞篡,以小見大的干貨文章哦~
掃下方的公眾號(hào)二維碼并點(diǎn)擊關(guān)注世蔗,期待與您的共同成長(zhǎng)~