建議查看原文:http://www.itdecent.cn/p/23c876f8ae39(不定時(shí)更新)
源碼剖析學(xué)習(xí)系列:(不斷更新)
1、FBKVOController源碼剖析與學(xué)習(xí)
2、MJRefresh源碼剖析與學(xué)習(xí)
3、YYImage源碼剖析與學(xué)習(xí)
MJRefresh是李明杰大神的開(kāi)源框架,這是一款十分優(yōu)雅的刷新組件庫(kù),這開(kāi)源組件無(wú)論從代碼風(fēng)格,可用性,易讀性還是兼容性來(lái)講都十分優(yōu)秀。本文就最新MJRefresh版本來(lái)講解。耐心看下去,本文和純解讀源碼的文章不同。本文碼字幾天,如果對(duì)您有幫助,給個(gè)鼓勵(lì),謝謝大家!
MJRefresh
一、MJRefreshComponent
1.導(dǎo)入文件
#import <UIKit/UIKit.h>
#import "MJRefreshConst.h"
#import "UIView+MJExtension.h"
#import "UIScrollView+MJExtension.h"
#import "UIScrollView+MJRefresh.h"
#import "NSBundle+MJRefresh.h"
2.狀態(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
};
3、刷新回調(diào)
#pragma mark - 刷新回調(diào)
/** 正在刷新的回調(diào) */
@property (copy, nonatomic) MJRefreshComponentRefreshingBlock refreshingBlock;
/** 設(shè)置回調(diào)對(duì)象和回調(diào)方法 */
- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action;
/** 回調(diào)對(duì)象 */
@property (weak, nonatomic) id refreshingTarget;
/** 回調(diào)方法 */
@property (assign, nonatomic) SEL refreshingAction;
/** 觸發(fā)回調(diào)(交給子類去調(diào)用) */
- (void)executeRefreshingCallback;
4、刷新?tīng)顟B(tài)控制
#pragma mark - 刷新?tīng)顟B(tài)控制
/** 進(jìn)入刷新?tīng)顟B(tài) */
- (void)beginRefreshing;
- (void)beginRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 開(kāi)始刷新后的回調(diào)(進(jìn)入刷新?tīng)顟B(tài)后的回調(diào)) */
@property (copy, nonatomic) MJRefreshComponentbeginRefreshingCompletionBlock beginRefreshingCompletionBlock;
/** 結(jié)束刷新的回調(diào) */
@property (copy, nonatomic) MJRefreshComponentEndRefreshingCompletionBlock endRefreshingCompletionBlock;
/** 結(jié)束刷新?tīng)顟B(tài) */
- (void)endRefreshing;
- (void)endRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 是否正在刷新 */
@property (assign, nonatomic, readonly, getter=isRefreshing) BOOL refreshing;
//- (BOOL)isRefreshing;
/** 刷新?tīng)顟B(tài) 一般交給子類內(nèi)部實(shí)現(xiàn) */
@property (assign, nonatomic) MJRefreshState state;
具體方法分析:
#pragma mark 進(jìn)入刷新?tīng)顟B(tài)
- (void)beginRefreshing
{
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.alpha = 1.0;
}];
self.pullingPercent = 1.0;
// 只要正在刷新,就完全顯示
if (self.window) {
self.state = MJRefreshStateRefreshing;
} else {
// 預(yù)防正在刷新中時(shí),調(diào)用本方法使得header inset回置失敗
if (self.state != MJRefreshStateRefreshing) {
self.state = MJRefreshStateWillRefresh;
// 刷新(預(yù)防從另一個(gè)控制器回到這個(gè)控制器的情況,回來(lái)要重新刷新一下)
[self setNeedsDisplay];
}
}
}
上面做了一個(gè)動(dòng)畫(huà)效果,多加了一個(gè)
willRefresh的狀態(tài),我的理解是為了防止self.window為空的時(shí)候,突然刷新崩潰(從另一個(gè)頁(yè)面返回的時(shí)候),所以需要一個(gè)狀態(tài)來(lái)過(guò)渡。
設(shè)置
state會(huì)調(diào)用setNeedsLayout方法;如果self.window為空,把狀態(tài)改成即將刷新,并調(diào)用setNeedsDisplay
- 首先
UIView的setNeedsDisplay和setNeedsLayout方法都是異步執(zhí)行的。而setNeedsDisplay會(huì)調(diào)用自動(dòng)調(diào)用drawRect方法,這樣可以拿到UIGraphicsGetCurrentContext,就可以繪制了,而setNeedsLayout會(huì)默認(rèn)調(diào)用layoutSubViews,就可以處理子視圖中的一些數(shù)據(jù)。- 綜上所訴,
setNeedsDisplay方便繪圖,而layoutSubViews方便出來(lái)數(shù)據(jù)。
//結(jié)束刷新
- (void)endRefreshing
{
dispatch_async(dispatch_get_main_queue(), ^{
self.state = MJRefreshStateIdle;
});
}
在主線程結(jié)束刷新,把刷新?tīng)顟B(tài)改為普通閑置狀態(tài)
5、KVO監(jiān)聽(tīng)
#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];
}
- (void)removeObservers
{
[self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentOffset];
[self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentSize];
[self.pan removeObserver:self forKeyPath:MJRefreshKeyPathPanState];
self.pan = nil;
}
- (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];
}
}
監(jiān)聽(tīng)
ContentOffset、ContentSize、手勢(shì)的State
6、回調(diào)
#pragma mark - 內(nèi)部方法
- (void)executeRefreshingCallback
{
dispatch_async(dispatch_get_main_queue(), ^{
if (self.refreshingBlock) {
self.refreshingBlock();
}
if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
}
if (self.beginRefreshingCompletionBlock) {
self.beginRefreshingCompletionBlock();
}
});
}
MJRefreshMsgSend是時(shí)運(yùn)行時(shí)objc_msgSend,第一個(gè)參數(shù)代表接收者,第二個(gè)參數(shù)代表選擇子(SEL是選擇子的類型),后續(xù)參數(shù)就是消息中的那些參數(shù),其順序不變。選擇子指的就是方法的名字。
二、MJRefreshHeader
1、初始化(構(gòu)造方法)
#pragma mark - 構(gòu)造方法
+ (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、覆蓋父類方法
- (void)prepare
{
[super prepare];
// 設(shè)置key
self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
// 設(shè)置高度
self.mj_h = MJRefreshHeaderHeight;
}
- (void)placeSubviews
{
[super placeSubviews];
// 設(shè)置y值(當(dāng)自己(頭部)的高度發(fā)生改變了,肯定要重新調(diào)整Y值,所以放到placeSubviews方法中設(shè)置y值)
self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}
prepare設(shè)置一下初始化值數(shù)據(jù)。而placeSubViews更新一下UI。
3、滾動(dòng)時(shí)偏移值變化以及狀態(tài)的改變
//當(dāng)scrollView的contentOffset發(fā)生改變的時(shí)候調(diào)用
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing狀態(tài)
if (self.state == MJRefreshStateRefreshing) {
// 暫時(shí)保留
if (self.window == nil) return;
// sectionheader停留解決
//刷新的時(shí)候:偏移量(self.scrollView.mj_offsetY) = 狀態(tài)欄 + 導(dǎo)航欄 + header的高度(54+64=118 iphoneX則為54+88=142)
//內(nèi)邊距高度(_scrollViewOriginalInset.top)= 狀態(tài)欄 + 導(dǎo)航欄 = 64
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)到下一個(gè)控制器時(shí),contentInset可能會(huì)變
_scrollViewOriginalInset = self.scrollView.mj_inset;
// 當(dāng)前的contentOffset
CGFloat offsetY = self.scrollView.mj_offsetY;
// 頭部控件剛好出現(xiàn)的offsetY
CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
// 如果是向上滾動(dòng)到看不見(jiàn)頭部控件,直接返回
// >= -> >
if (offsetY > happenOffsetY) return;
// 普通閑置 即將刷新 的臨界點(diǎn)
//個(gè)人覺(jué)得normal2pullingOffsetY應(yīng)該是頭部完全出來(lái)時(shí)的Y軸偏移值
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) { //手指拖拽中,狀態(tài)是默認(rèn)狀態(tài)以及下拉距離(偏移值)大于臨界點(diǎn)距離
// 轉(zhuǎn)為可以進(jìn)行刷新?tīng)顟B(tài)
self.state = MJRefreshStatePulling;
} else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
//手指拖拽中,狀態(tài)是默認(rèn)狀態(tài)以及下拉距離(偏移值)小于臨界點(diǎn)距離,也就是拖得比較下
// 轉(zhuǎn)為普通狀態(tài)
self.state = MJRefreshStateIdle;
}
} else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開(kāi)
// 開(kāi)始刷新
[self beginRefreshing];
} else if (pullingPercent < 1) {
self.pullingPercent = pullingPercent;//手松開(kāi)后,默認(rèn)狀態(tài)時(shí),恢復(fù)self.pullingPercent
}
}
狀態(tài)切換的因素有兩個(gè):一個(gè)是下拉的距離是否超過(guò)臨界值,另一個(gè)是 手指是否離開(kāi)屏幕。
手指還貼在屏幕的時(shí)候是不能進(jìn)行刷新的。即使在下拉的距離超過(guò)了臨界距離(狀態(tài)欄 + 導(dǎo)航欄 + header高度),如果手指沒(méi)有離開(kāi)屏幕,那么也不能馬上進(jìn)行刷新,而是將狀態(tài)切換為:可以刷新。一旦手指離開(kāi)了屏幕,馬上將狀態(tài)切換為正在刷新。
普通閑置與即將刷新的分界點(diǎn),看下圖,一目了然
4、改變狀態(tài)時(shí)的相應(yīng)操作(setter方法)
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
//MJRefreshCheckState是宏,其實(shí)也就是下面語(yǔ)句,為了檢測(cè)狀態(tài)是否相同,相同則return
// MJRefreshState oldState = self.state;
// if (state == oldState) {
// NSLog(@"相同");
// return;
// }
// [super setState:state];
// 根據(jù)狀態(tài)做事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;
// 保存刷新時(shí)間
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
[[NSUserDefaults standardUserDefaults] synchronize];
// 恢復(fù)inset和offset
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
//此時(shí)要加上scrollView刷新時(shí)跟普通閑置時(shí)的偏移差值(刷新時(shí)偏移值為118或者142,self.insetTDelta值為header高度-54),恢復(fù)后self.scrollView.mj_insetT = 64(或者88)
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) {
self.endRefreshingCompletionBlock();
}
}];
} else if (state == MJRefreshStateRefreshing) {
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
// 增加滾動(dòng)區(qū)域top
self.scrollView.mj_insetT = top;
//增加滾動(dòng)區(qū)域top(賦值給scrollView.inset.top)
CGPoint offset = self.scrollView.contentOffset;
offset.y = -top;
[self.scrollView setContentOffset:offset animated:NO];
} completion:^(BOOL finished) {
//執(zhí)行正在刷新的回調(diào)
[self executeRefreshingCallback];
}];
});
}
}
注意
[super setState:state]的位置,等基類的state賦值給oldState,再跟新?tīng)顟B(tài)對(duì)比,對(duì)比完后,再[super setState:state]調(diào)用基類,從而賦值基類state
該方法主要要注意狀態(tài)在普通閑置狀態(tài)以及刷新?tīng)顟B(tài)的scrollView.contentOffset變化
三、MJRefreshStateHeader
該類是MJRefreshHeader的子類,主要用來(lái)設(shè)置顯示上一次刷新時(shí)間的label:lastUpdatedTimeLabel和顯示刷新?tīng)顟B(tài)的label:stateLabel屬性等
1、stateLabel初始化方法
- (void)setTitle:(NSString *)title forState:(MJRefreshState)state
{
if (title == nil) return;
self.stateTitles[@(state)] = title;
self.stateLabel.text = self.stateTitles[@(self.state)];
}
#pragma mark - 覆蓋父類的方法
- (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];
}
prepare初始化方法,實(shí)現(xiàn)本地化(不同字體),并根據(jù)不同狀態(tài)賦值給stateLabel
2、lastUpdatedLabel賦值
#pragma mark key的處理
- (void)setLastUpdatedTimeKey:(NSString *)lastUpdatedTimeKey
{
[super setLastUpdatedTimeKey:lastUpdatedTimeKey];
// 如果label隱藏了,就不用再處理
if (self.lastUpdatedTimeLabel.hidden) return;
NSDate *lastUpdatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:lastUpdatedTimeKey];
// 如果有block
//用戶定義的時(shí)間格式
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"; //返回11:11樣式
isToday = YES;
} else if ([cmp1 year] == [cmp2 year]) { // 今年
formatter.dateFormat = @"MM-dd HH:mm"; //返回02-08 11:11樣式
} else {
formatter.dateFormat = @"yyyy-MM-dd HH:mm"; //返回2018-02-08 11:11樣式
}
NSString *time = [formatter stringFromDate:lastUpdatedTime];
// 3.顯示日期
//[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText] 會(huì)返回簡(jiǎn)體(英文、繁體)的 【最后更新:】
//isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"" 如果上一次刷新也是今天,則返回簡(jiǎn)體(英文、繁體)的 【今天】,不是則返回空字符串
self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@%@",
[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"",
time];
} else {
//沒(méi)有獲得上次更新時(shí)間
self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@",
[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
[NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];
}
}
注意一下時(shí)間格式,本地化以及不同上次刷新時(shí)間的
lastUpdatedTimeLabel顯示
上面代碼還給用戶自定義時(shí)間格式,沒(méi)有才使用默認(rèn),默認(rèn)的格式邏輯顯示,我已在上面注釋清楚
MJRefreshNormalHeader和MJRefreshGifHeader都是MJRefreshStateHeader的子類,前者和后者的布局一樣,不同的就是header左邊一個(gè)是菊花的樣式,另外一個(gè)是gif,詳看下圖:
由此看來(lái),這兩種形式的
header都有相同的共性,我們?cè)谧鲱愃频墓δ軙r(shí),如果有幾個(gè)控件或者幾個(gè)類共性一樣,比如說(shuō),一個(gè)保險(xiǎn)類(InsuranceClass),一個(gè)房地產(chǎn)類(RealEstateClass),他們可以有一個(gè)基類銷售類(SalesClass),SalesClass擁有銷售員工、顧客、金額、銷售日期等 保險(xiǎn)類 和 房地產(chǎn)類 需要的共同屬性
四、MJRefreshNormalHeader
1、在
MJRefreshStateHeader上添加了箭頭和菊花
2、布局這兩種樣式
View,且在狀態(tài)切換時(shí)更改樣式切換
1、圈圈(菊花)和箭頭的布局
- (void)placeSubviews
{
[super placeSubviews];
// 箭頭的中心點(diǎn)
CGFloat arrowCenterX = self.mj_w * 0.5;
if (!self.stateLabel.hidden) {
CGFloat stateWidth = self.stateLabel.mj_textWith; //狀態(tài)label文字的寬度
CGFloat timeWidth = 0.0;
if (!self.lastUpdatedTimeLabel.hidden) {
timeWidth = self.lastUpdatedTimeLabel.mj_textWith; //時(shí)間label文字的寬度
}
CGFloat textWidth = MAX(stateWidth, timeWidth); //求出一個(gè)最寬的文字寬度
arrowCenterX -= textWidth / 2 + self.labelLeftInset; //箭頭(菊花)中心點(diǎn)x還要減去(最寬的文字寬度/2 + 文字距離圈圈、箭頭的距離)
}
//中心點(diǎn)y設(shè)置為header的高度的一半
CGFloat arrowCenterY = self.mj_h * 0.5;
CGPoint arrowCenter = CGPointMake(arrowCenterX, arrowCenterY);
// 箭頭
if (self.arrowView.constraints.count == 0) { //箭頭沒(méi)有其他布局約束
self.arrowView.mj_size = self.arrowView.image.size; //箭頭大小跟提供的arrowView圖片大小一致
self.arrowView.center = arrowCenter;
}
// 圈圈
if (self.loadingView.constraints.count == 0) { //圈圈(菊花)沒(méi)有其他布局約束
self.loadingView.center = arrowCenter;
}
self.arrowView.tintColor = self.stateLabel.textColor;
}
上面代碼主要實(shí)現(xiàn)了圈圈(菊花)和箭頭的布局,需要注意的是讓箭頭菊花緊跟刷新文字或者狀態(tài)文字居中的邏輯,我已在注釋寫(xiě)明
2、不同狀態(tài)下菊花和箭頭的互換
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根據(jù)狀態(tài)做事情
if (state == MJRefreshStateIdle) {
if (oldState == MJRefreshStateRefreshing) { //上次狀態(tài)是正在刷新,準(zhǔn)備改變成普通閑置狀態(tà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;
// self.loadingView.backgroundColor = [UIColor greenColor];
self.loadingView.alpha = 1.0; //菊花變成完全顯示 (為什么要這樣?求大佬告訴)
[self.loadingView stopAnimating]; //菊花停止轉(zhuǎn)動(dòng),同時(shí)會(huì)隱藏菊花(loadingView.hidesWhenStopped = YES;)
self.arrowView.hidden = NO; //箭頭顯示
}];
} else { //上次狀態(tài)是拖拽或者普通閑置狀態(tài),準(zhǔn)備改變成普通閑置狀態(tài) --> 把菊花停止轉(zhuǎn)動(dòng),菊花隱藏,箭頭顯示
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowView.transform = CGAffineTransformIdentity; //在操作結(jié)束之后對(duì)箭頭設(shè)置量進(jìn)行還原
}];
}
} else if (state == MJRefreshStatePulling) { //拖拽狀態(tài):菊花停止轉(zhuǎn)動(dòng),箭頭顯示
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);//(改變箭頭的方向,但是為什么要0.000001 - M_PI?)
}];
} else if (state == MJRefreshStateRefreshing) { //正在刷新?tīng)顟B(tài):菊花完全顯示并且開(kāi)始轉(zhuǎn)動(dòng),箭頭隱藏
self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動(dòng)畫(huà)完畢動(dòng)作沒(méi)有被執(zhí)行
[self.loadingView startAnimating];
self.arrowView.hidden = YES;
}
}
通過(guò)不同的狀態(tài)控制菊花和箭頭的隱藏和消失,及他們的動(dòng)畫(huà)效果,如箭頭的朝上朝下,和菊花的轉(zhuǎn)與不轉(zhuǎn)
四、MJRefreshGifHeader
1、加載不同狀態(tài)對(duì)應(yīng)的動(dòng)畫(huà)圖片
2、設(shè)置不同狀態(tài)對(duì)應(yīng)的動(dòng)畫(huà)時(shí)間
1、懶加載
#pragma mark - 懶加載
//gigView顯示gif
- (UIImageView *)gifView
{
if (!_gifView) {
UIImageView *gifView = [[UIImageView alloc] init];
[self addSubview:_gifView = gifView];
}
return _gifView;
}
- (NSMutableDictionary *)stateImages
{
if (!_stateImages) {
self.stateImages = [NSMutableDictionary dictionary];
}
return _stateImages;
}
- (NSMutableDictionary *)stateDurations
{
if (!_stateDurations) {
self.stateDurations = [NSMutableDictionary dictionary];
}
return _stateDurations;
}
2、設(shè)置不通過(guò)狀態(tài)對(duì)應(yīng)的動(dòng)畫(huà)圖片以及動(dòng)畫(huà)時(shí)間
#pragma mark - 公共方法
- (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];
}
3、實(shí)現(xiàn)圖片的切換和gifView布局
#pragma mark - 實(shí)現(xiàn)父類的方法
- (void)prepare
{
[super prepare];
// 初始化間距
self.labelLeftInset = 20;
}
//根據(jù)拖拽進(jìn)度設(shè)置透明度
- (void)setPullingPercent:(CGFloat)pullingPercent
{
[super setPullingPercent:pullingPercent];
NSArray *images = self.stateImages[@(MJRefreshStateIdle)]; //選擇閑置狀態(tài)下的圖片組
if (self.state != MJRefreshStateIdle || images.count == 0) return; //狀態(tài)不是閑置或者圖片為空,則直接返回
// 停止動(dòng)畫(huà)
[self.gifView stopAnimating];
// 設(shè)置當(dāng)前需要顯示的圖片
NSUInteger index = images.count * pullingPercent;
if (index >= images.count) index = images.count - 1;
self.gifView.image = images[index];
}
- (void)placeSubviews
{
[super placeSubviews];
if (self.gifView.constraints.count) return; //gifView沒(méi)有約束,直接返回
self.gifView.frame = self.bounds;
if (self.stateLabel.hidden && self.lastUpdatedTimeLabel.hidden) { //上次刷新時(shí)間和狀態(tài)文字都隱藏了,圖片內(nèi)容就居ifView中間顯示
self.gifView.contentMode = UIViewContentModeCenter;
} else { //圖片居gifView右邊顯示
self.gifView.contentMode = UIViewContentModeRight;
//下面代碼同樣也是為了讓gifView緊挨著文字居中顯示。算出最長(zhǎng)的文字,通過(guò)減去文字的一般寬度,調(diào)整gifView的x值,跟NormalHeader的方法一樣,詳細(xì)請(qǐng)看normalHeader
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;
}
}
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根據(jù)狀態(tài)做事情
if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) { //狀態(tài)變?yōu)橥献Щ蛘哒谒⑿?,獲取各自狀態(tài)該顯示的圖片組
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];
}
}
到此,我對(duì)MJRefreshHeader那一塊的源碼已經(jīng)讀完,剩下MJRefreshFooter,但由于實(shí)現(xiàn)邏輯基本一致,故在此不再詳說(shuō)。遲點(diǎn),發(fā)現(xiàn)MJRefreshFooter有其他特殊之處,我會(huì)更新此文,謝謝大家!
學(xué)習(xí)
1、巧用Model
我們可能見(jiàn)到一些開(kāi)發(fā)者會(huì)在didSelectRowAtIndexPath協(xié)議方法里面這樣寫(xiě)
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
MJExample *exam = self.examples[indexPath.section];
UIViewController *vc = [[exam.vcClass alloc] init];
vc.title = exam.titles[indexPath.row];
[vc setValue:exam.methods[indexPath.row] forKeyPath:@"method"];
[self.navigationController pushViewController:vc animated:YES];
if (indexPath.row == 0) {
UIViewController *test1 = [UIViewController new];
test1.title = @"test1";
[self.navigationController pushViewController:test1 animated:YES];
}else if (indexPath.row == 1) {
UIViewController *test2 = [UIViewController new];
test2.title = @"test2";
[self.navigationController pushViewController:test2 animated:YES];
}else if (indexPath.row == 2) {
UIViewController *test3 = [UIViewController new];
test3.title = @"test3";
[self.navigationController pushViewController:test3 animated:YES];
}else {
;
}
}
這樣會(huì)造成
didSelectRowAtIndexPath方法過(guò)于臃腫,且重復(fù)代碼過(guò)多,太多if else 或者 switch,我們可以用Model很好的解決這個(gè)問(wèn)題,代碼如下:
- (NSArray *)examples
{
if (!_examples) {
MJExample *exam0 = [[MJExample alloc] init];
exam0.header = MJExample00;
exam0.vcClass = [MJTableViewController class];
exam0.titles = @[@"默認(rèn)", @"動(dòng)畫(huà)圖片", @"隱藏時(shí)間", @"隱藏狀態(tài)和時(shí)間", @"自定義文字", @"自定義刷新控件"];
exam0.methods = @[@"example01", @"example02", @"example03", @"example04", @"example05", @"example06"];
MJExample *exam1 = [[MJExample alloc] init];
exam1.header = MJExample10;
exam1.vcClass = [MJTableViewController class];
exam1.titles = @[@"默認(rèn)", @"動(dòng)畫(huà)圖片", @"隱藏刷新?tīng)顟B(tài)的文字", @"全部加載完畢", @"禁止自動(dòng)加載", @"自定義文字", @"加載后隱藏", @"自動(dòng)回彈的上拉01", @"自動(dòng)回彈的上拉02", @"自定義刷新控件(自動(dòng)刷新)", @"自定義刷新控件(自動(dòng)回彈)"];
exam1.methods = @[@"example11", @"example12", @"example13", @"example14", @"example15", @"example16", @"example17", @"example18", @"example19", @"example20", @"example21"];
MJExample *exam2 = [[MJExample alloc] init];
exam2.header = MJExample20;
exam2.vcClass = [MJCollectionViewController class];
exam2.titles = @[@"上下拉刷新"];
exam2.methods = @[@"example21"];
MJExample *exam3 = [[MJExample alloc] init];
exam3.header = MJExample30;
exam3.vcClass = [MJWebViewViewController class];
exam3.titles = @[@"下拉刷新"];
exam3.methods = @[@"example31"];
self.examples = @[exam0, exam1, exam2, exam3];
}
return _examples;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
MJExample *exam = self.examples[indexPath.section];
UIViewController *vc = [[exam.vcClass alloc] init];
vc.title = exam.titles[indexPath.row];
[vc setValue:exam.methods[indexPath.row] forKeyPath:@"method"];
[self.navigationController pushViewController:vc animated:YES];
}
2、跳轉(zhuǎn)巧用
ViewController.h
- (IBAction)tappdeBtn:(id)sender {
UIViewController *vc = [[BViewController alloc] init];
vc.title = @"example01";
[vc setValue:@"example01" forKeyPath:@"method"];
[self.navigationController pushViewController:vc animated:YES];
}
上面是跳轉(zhuǎn)方法,請(qǐng)留意
[vc setValue:@"example01" forKeyPath:@"method"];這句代碼,下面會(huì)詳解
BViewController.h
#import "BViewController.h"
#import "UIViewController+Example.h"
#define MJPerformSelectorLeakWarning(Stuff) \
do { \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
Stuff; \
_Pragma("clang diagnostic pop") \
} while (0)
@implementation BViewController
- (void)viewDidLoad {
[super viewDidLoad];
MJPerformSelectorLeakWarning(
[self performSelector:NSSelectorFromString(self.method) withObject:nil];
);
}
- (void)example01
{
NSLog(@"進(jìn)入此方法");
}
結(jié)果:
1、由上可以看到
[self performSelector:NSSelectorFromString(self.method) withObject:nil];沒(méi)有指明方法名,仍可以調(diào)用- (void)example01(),這是運(yùn)用了runtime的黑魔法,定義了UIViewController+Example分類方法,runtime的使用可以看我之前的文章-->iOS進(jìn)階之runtime作用
2、
MJPerformSelectorLeakWarning( );如果selector是在運(yùn)行時(shí)才確定的,performSelector時(shí),若先把selector保存起來(lái),等到某事件發(fā)生后再調(diào)用,相當(dāng)于在動(dòng)態(tài)綁定之上再使用動(dòng)態(tài)綁定,不過(guò)這是編譯器不知道要執(zhí)行的selector是什么,因?yàn)檫@必須到了運(yùn)行時(shí)才能確定,使用這種特性的代價(jià)是,如果在ARC下編譯代碼,編譯器會(huì)發(fā)生警告,可用#pragma clang diagnostic ignored "-Warc-performSelector-leaks"忽略警告
#import <UIKit/UIKit.h>
@interface UIViewController (Example)
@property (copy, nonatomic) NSString *method;
@end
----------------------------
#import "UIViewController+Example.h"
#import <objc/runtime.h>
@implementation UIViewController (Example)
static char MethodKey;
- (void)setMethod:(NSString *)method
{
objc_setAssociatedObject(self, &MethodKey, method, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)method
{
return objc_getAssociatedObject(self, &MethodKey);
}
這是runtime中為分類添加屬性的經(jīng)典用法,把上面跳轉(zhuǎn)方法中的
[vc setValue:@"example01" forKeyPath:@"method"];賦值的example01利用runtime關(guān)聯(lián),這樣分類中的method屬性值就為example01
解析一下 static char
比如有這樣一個(gè)函數(shù)
exp()
{
char a[] = "Hello!" ;
static char b[] = "Hello!" ;
}
當(dāng)調(diào)用這個(gè)函數(shù)完后,a[]就不存在了,而b[]依然存在,并且值為hello;
參考:
performSelector系列方法編譯器警告-Warc-performSelector-leaks