博主寫的特別棒宁舰,在此收藏一遍以防丟失,供自己學(xué)習(xí)參考之用奢浑。鏈接如下:
MJRefresh源碼解析
MJRefresh是李明杰老師的作品明吩,到現(xiàn)在已經(jīng)有9800多顆star了,是一個簡單實用殷费,功能強(qiáng)大的iOS下拉刷新(也支持上拉加載更多)控件印荔。它的可定制性很高,幾乎可以滿足大部分下拉刷新的設(shè)計需求详羡,值得學(xué)習(xí)仍律。
該框架的結(jié)構(gòu)設(shè)計得很清晰,使用一個基類MJRefreshComponent
來做一些基本的設(shè)定实柠,然后通過繼承的方式水泉,讓MJRefreshHeader
和MJRefreshFooter
分別具備下拉刷新和上拉加載的功能。從繼承機(jī)構(gòu)來看可以分為三層窒盐,具體可以從下面的圖里看出來:
首先來看一下該控件的基類:MJRefreshComponent:
MJRefreshComponent
這個類作為該控件的基類草则,涵蓋了基類所具備的一些:狀態(tài),回調(diào)block等蟹漓,大致分成下面這5種職能:
- 聲明控件的所有狀態(tài)炕横。
- 聲明控件的回調(diào)函數(shù)。
- 添加監(jiān)聽葡粒。
- 提供刷新份殿,停止刷新接口膜钓。
- 提供子類需要實現(xiàn)的方法。
職能如何實現(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)(void);
/** 開始刷新后的回調(diào)(進(jìn)入刷新狀態(tài)后的回調(diào)) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)(void);
/** 結(jié)束刷新后的回調(diào) */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)(void);
3. 添加監(jiān)聽
監(jiān)聽的聲明:
- (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];
}
對于監(jiān)聽的處理:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// 遇到這些情況就直接返回
if (!self.userInteractionEnabled) return;
// 這個就算看不見也需要處理
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)beginRefreshing
{
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.alpha = 1.0;
}];
self.pullingPercent = 1.0;
// 只要正在刷新拾枣,就完全顯示
if (self.window) {
self.state = MJRefreshStateRefreshing;
} else {
// 預(yù)防正在刷新中時沃疮,調(diào)用本方法使得header inset回置失敗
if (self.state != MJRefreshStateRefreshing) {
self.state = MJRefreshStateWillRefresh;
// 刷新(預(yù)防從另一個控制器回到這個控制器的情況,回來要重新刷新一下)
[self setNeedsDisplay];
}
}
}
- (void)beginRefreshingWithCompletionBlock:(void (^)(void))completionBlock
{
self.beginRefreshingCompletionBlock = completionBlock;
[self beginRefreshing];
}
#pragma mark 結(jié)束刷新狀態(tài)
- (void)endRefreshing
{
dispatch_async(dispatch_get_main_queue(), ^{
self.state = MJRefreshStateIdle;
});
}
- (void)endRefreshingWithCompletionBlock:(void (^)(void))completionBlock
{
self.endRefreshingCompletionBlock = completionBlock;
[self endRefreshing];
}
5. 提供子類需要實現(xiàn)的方法
/** 初始化 */
- (void)prepare NS_REQUIRES_SUPER;
/** 擺放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的contentOffset發(fā)生改變的時候調(diào)用 */
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的contentSize發(fā)生改變的時候調(diào)用 */
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當(dāng)scrollView的拖拽狀態(tài)發(fā)生改變的時候調(diào)用 */
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
從上面等結(jié)構(gòu)圖可以看出梅肤,緊接著這個基類忿磅,下面分為MJRefreshHeader
和MJRefreshFooter
,這里順著MJRefreshHeader
這個分支向下展開:
MJRefreshHeader
MJRefreshHeader
繼承于MJRefreshComponent
凭语,它做了這幾件事:
有哪些職能葱她?
- 初始化。
- 設(shè)置header高度似扔。
- 重新調(diào)整y值吨些。
- 根據(jù)
contentOffset
的變化,來切換狀態(tài)(默認(rèn)狀態(tài)炒辉,可以刷新的狀態(tài)豪墅,正在刷新的狀態(tài)),實現(xiàn)方法是:scrollViewContentOffsetDidChange:
黔寇。 - 在切換狀態(tài)時偶器,執(zhí)行相應(yīng)的操作。實現(xiàn)方法是:
setState:
缝裤。
職能如何實現(xiàn)屏轰?
1. 初始化
初始化有兩種方法:
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
MJRefreshHeader *cmp = [[self alloc] init];
cmp.refreshingBlock = refreshingBlock;
return cmp;
}
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
MJRefreshHeader *cmp = [[self alloc] init];
[cmp setRefreshingTarget:target refreshingAction:action];
return cmp;
}
2. 設(shè)置header高度
通過重寫prepare
方法來設(shè)置header的高度:
- (void)prepare
{
[super prepare];
// 設(shè)置key
self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
// 設(shè)置高度
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;
}
4. 狀態(tài)切換的代碼:
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing狀態(tài)
if (self.state == MJRefreshStateRefreshing) {
// if (self.window == nil) return;
// sectionheader停留解決
CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
self.scrollView.mj_insetT = insetT;
self.insetTDelta = _scrollViewOriginalInset.top - insetT;
return;
}
// 跳轉(zhuǎn)到下一個控制器時霎苗,contentInset可能會變
_scrollViewOriginalInset = self.scrollView.mj_inset;
// 當(dāng)前的contentOffset
CGFloat offsetY = self.scrollView.mj_offsetY;
// 頭部控件剛好出現(xiàn)的offsetY
CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
// 如果是向上滾動到看不見頭部控件,直接返回
// >= -> >
if (offsetY > happenOffsetY) return;
// 普通 和 即將刷新 的臨界點
CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
if (self.scrollView.isDragging) { // 如果正在拖拽
self.pullingPercent = pullingPercent;
if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
// 轉(zhuǎn)為即將刷新狀態(tài)
self.state = MJRefreshStatePulling;
} else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
// 轉(zhuǎn)為普通狀態(tài)
self.state = MJRefreshStateIdle;
}
} else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
// 開始刷新
[self beginRefreshing];
} else if (pullingPercent < 1) {
self.pullingPercent = pullingPercent;
}
}
需要注意三點:
- 這里的狀態(tài)有三種:默認(rèn)狀態(tài)(MJRefreshStateIdle)榛做,可以刷新的狀態(tài)(MJRefreshStatePulling)以及正在刷新的狀態(tài)(MJRefreshStateRefreshing)唁盏。
- 狀態(tài)切換的因素有兩個:一個是下拉的距離是否超過臨界值,另一個是 手指是否離開屏幕检眯。
- 注意:可以刷新的狀態(tài)和正在刷新的狀態(tài)是不同的厘擂。因為在手指還貼在屏幕的時候是不能進(jìn)行刷新的。所以即使在下拉的距離超過了臨界距離(狀態(tài)欄 + 導(dǎo)航欄 + header高度)锰瘸,如果手指沒有離開屏幕刽严,那么也不能馬上進(jìn)行刷新,而是將狀態(tài)切換為:可以刷新获茬。一旦手指離開了屏幕港庄,馬上將狀態(tài)切換為正在刷新。
這里提供一張圖來體現(xiàn)三個狀態(tài)的不同:
5. 狀態(tài)切換時的相應(yīng)操作:
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根據(jù)狀態(tài)做事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;
// 保存刷新時間
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
[[NSUserDefaults standardUserDefaults] synchronize];
// 恢復(fù)inset和offset
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.scrollView.mj_insetT += self.insetTDelta;
// 自動調(diào)整透明度
if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
} completion:^(BOOL finished) {
self.pullingPercent = 0.0;
if (self.endRefreshingCompletionBlock) {
self.endRefreshingCompletionBlock();
}
}];
} else if (state == MJRefreshStateRefreshing) {
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
// 增加滾動區(qū)域top
self.scrollView.mj_insetT = top;
// 設(shè)置滾動位置
CGPoint offset = self.scrollView.contentOffset;
offset.y = -top;
[self.scrollView setContentOffset:offset animated:NO];
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
});
}
}
這里需要注意兩點:
- 這里狀態(tài)的切換恕曲,主要圍繞著兩種:默認(rèn)狀態(tài)和正在刷新狀態(tài)鹏氧。也就是針對開始刷新和結(jié)束刷新這兩個切換點。
- 從正在刷新狀態(tài)狀態(tài)切換為默認(rèn)狀態(tài)時(結(jié)束刷新)佩谣,需要記錄刷新結(jié)束的時間把还。因為header里面有一個默認(rèn)的label是用來顯示上次刷新的時間的。
MJRefreshStateHeader
這個類是MJRefreshHeader
類的子類茸俭,它做了兩件事:
有哪些職能吊履?
- 簡單布局了
stateLabel
和lastUpdatedTimeLabel
。 -
根據(jù)控件狀態(tài)的切換(默認(rèn)狀態(tài)调鬓,正在刷新狀態(tài))艇炎,實現(xiàn)了這兩個label顯示的文字的切換。
給一張圖腾窝,讓大家直觀感受一下這兩個控件:
兩個Label.png
職能如何實現(xiàn)缀踪?
這個類通過覆蓋父類三個方法來實現(xiàn)上述兩個實現(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];
}
在這里,將每一個狀態(tài)對應(yīng)的提示文字放入一個字典里面,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) {
// 狀態(tài)
if (noConstrainsOnStatusLabel) self.stateLabel.frame = self.bounds;
} else {
CGFloat stateLabelH = self.mj_h * 0.5;
// 狀態(tài)
if (noConstrainsOnStatusLabel) {
self.stateLabel.mj_x = 0;
self.stateLabel.mj_y = 0;
self.stateLabel.mj_w = self.mj_w;
self.stateLabel.mj_h = stateLabelH;
}
// 更新時間
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;
}
}
}
這里主要是對
lastUpdatedTimeLabel
和stateLabel
進(jìn)行布局驴娃。要注意lastUpdatedTimeLabel
隱藏的情況。
方法3: setState:方法
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 設(shè)置狀態(tài)文字
self.stateLabel.text = self.stateTitles[@(state)];
// 重新設(shè)置key(重新顯示時間)
self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}
在這里循集,根據(jù)傳入的state的不同唇敞,在
stateLabel
和lastUpdatedTimeLabel
里切換相應(yīng)的文字。
stateLabel
里的文字直接從stateTitles
字典里取出即可咒彤。
< -lastUpdatedTimeLabel
里的文字需要通過一個方法來取出即可疆柔。
- (void)setLastUpdatedTimeKey:(NSString *)lastUpdatedTimeKey
{
[super setLastUpdatedTimeKey:lastUpdatedTimeKey];
// 如果label隱藏了,就不用再處理
if (self.lastUpdatedTimeLabel.hidden) return;
NSDate *lastUpdatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:lastUpdatedTimeKey];
// 如果有block
if (self.lastUpdatedTimeText) {
self.lastUpdatedTimeLabel.text = self.lastUpdatedTimeText(lastUpdatedTime);
return;
}
if (lastUpdatedTime) {
// 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 {
self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@",
[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
[NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];
}
}
在這里注意兩點:
- 作者通過使用block來讓用戶自己定義日期現(xiàn)實的格式镶柱,如果用戶沒有自定義婆硬,就使用作者提供的默認(rèn)格式。
- 在默認(rèn)格式的設(shè)置里奸例,判斷了是否是今日彬犯,是否是今年的情況。在以后設(shè)計顯示時間的labe的時候可以借鑒一下查吊。
MJRefreshNormalHeader
MJRefreshNormalHeader 繼承于 MJRefreshStateHeader谐区,它主要做了兩件事:
有哪些職能?
- 它在MJRefreshStateHeader上添加了
_arrowView
和loadingView
逻卖。 -
布局了這兩個view并在Refresh控件的狀態(tài)切換的時候改變這兩個view的樣式宋列。
還是給一張圖來直觀感受一下這兩個view:
兩個view.png
職能如何實現(xiàn)?
同MJRefreshStateHeader一樣评也,也是重寫了父類的三個方法:
方法1:prepare
- (void)prepare
{
[super prepare];
self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
}
方法2:placeSubviews
- (void)placeSubviews
{
[super placeSubviews];
// 箭頭的中心點
CGFloat arrowCenterX = self.mj_w * 0.5;
if (!self.stateLabel.hidden) {
CGFloat stateWidth = self.stateLabel.mj_textWith;
CGFloat timeWidth = 0.0;
if (!self.lastUpdatedTimeLabel.hidden) {
timeWidth = self.lastUpdatedTimeLabel.mj_textWith;
}
CGFloat textWidth = MAX(stateWidth, timeWidth);
arrowCenterX -= textWidth / 2 + self.labelLeftInset;
}
CGFloat arrowCenterY = self.mj_h * 0.5;
CGPoint arrowCenter = CGPointMake(arrowCenterX, arrowCenterY);
// 箭頭
if (self.arrowView.constraints.count == 0) {
self.arrowView.mj_size = self.arrowView.image.size;
self.arrowView.center = arrowCenter;
}
// 圈圈
if (self.loadingView.constraints.count == 0) {
self.loadingView.center = arrowCenter;
}
self.arrowView.tintColor = self.stateLabel.textColor;
}
在這里注意一點:因為
stateLabel
和lastUpdatedTimeLabel
是上下并排分布的炼杖,而arrowView
或loadingView
是在這二者的左邊灭返,所以為了避免這兩組重合,在計算arrowView
或loadingView
的center
的時候坤邪,需要獲取stateLabel
和lastUpdatedTimeLabel
兩個控件的寬度并比較大小熙含,將較大的一個作為兩個label的‘最寬距離’,再計算center艇纺,這樣一來就不會重合了怎静。
而對于如何計算寬度,作者給出了一個方案黔衡,大家可以在以后的實踐中使用蚓聘。
- (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)做事情
if (state == MJRefreshStateIdle) {
if (oldState == MJRefreshStateRefreshing) {
self.arrowView.transform = CGAffineTransformIdentity;
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.loadingView.alpha = 0.0;
} completion:^(BOOL finished) {
// 如果執(zhí)行完動畫發(fā)現(xiàn)不是idle狀態(tài),就直接返回盟劫,進(jìn)入其他狀態(tài)
if (self.state != MJRefreshStateIdle) return;
self.loadingView.alpha = 1.0;
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
}];
} else {
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowView.transform = CGAffineTransformIdentity;
}];
}
} else if (state == MJRefreshStatePulling) {
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
}];
} else if (state == MJRefreshStateRefreshing) {
self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動畫完畢動作沒有被執(zhí)行
[self.loadingView startAnimating];
self.arrowView.hidden = YES;
}
}
到此為止夜牡,我們已經(jīng)從MJRefreshComponent
到MJRefreshNormalHeader
的實現(xiàn)過程看了一遍÷虑可以看出氯材,作者將prepare
,placeSubviews
以及setState:
方法作為基類的方法硝岗,讓下面的子類去一層一層實現(xiàn)氢哮。
而每一層的子類,根據(jù)自身的職責(zé)型檀,分別按照自己的方式來實現(xiàn)這三個方法:
-
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
屬于同一級,都是繼承于MJRefreshStateHeader
无埃。因為二者都具有相同形式的stateLabel
和lastUpdatedTimeLabel
徙瓶,唯一不同的就是左側(cè)的部分:
-
MJRefreshNormalHeader
的左側(cè)是箭頭。 -
MJRefreshGifHeader
的左側(cè)則是一個gif動畫嫉称。
還是提供一張圖來直觀感受一下:
normalHeader 與 gifHeader.png
下面我們來看一下的實現(xiàn):
MJRefreshGifHeader
它提供了兩個接口侦镇,是用來設(shè)置不同狀態(tài)下使用的圖片數(shù)組的:
- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state
{
if (images == nil) return;
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
{
[self setImages:images duration:images.count * 0.1 forState:state];
}
有哪些職能?
然后织阅,和MJRefreshNormalHeader
一樣壳繁,它也重寫了基類提供的三個方法來實現(xiàn)顯示gif圖片的職能。
職能如何實現(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) {
self.gifView.contentMode = UIViewContentModeCenter;
} else {
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è)置動畫
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根據(jù)狀態(tài)做事情
if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {
NSArray *images = self.stateImages[@(state)];
if (images.count == 0) return;
[self.gifView stopAnimating];
if (images.count == 1) { // 單張圖片
self.gifView.image = [images lastObject];
} else { // 多張圖片
self.gifView.animationImages = images;
self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
[self.gifView startAnimating];
}
} else if (state == MJRefreshStateIdle) {
[self.gifView stopAnimating];
}
}
Footer類是用來處理上拉加載的闹炉,實現(xiàn)原理和下拉刷新很類似蒿赢,在這里先不介紹了~
總的來說,該框架設(shè)計得非常工整:通過一個基類來定義一些狀態(tài)和一些需要子類實現(xiàn)的接口渣触。通過一層一層地繼承羡棵,讓每一層的子類各司其職,只完成真正屬于自己的任務(wù)昵观,提高了框架的可定制性晾腔,而且對于功能的擴(kuò)展和bug的追蹤也很有幫助舌稀,非常值得我們參考與借鑒啊犬。