前言
在MJRefresh源碼閱讀1——結(jié)構(gòu)梳理中我們已經(jīng)說了MJRefreshHeader
是整個(gè)控件的核心類综看,它完成了一個(gè)刷新控件應(yīng)該有的所有邏輯和UI顯示红碑,它已經(jīng)是個(gè)成型的羡鸥,較簡(jiǎn)單的惧浴,麻雀雖小五臟俱全的刷新頭header
衷旅。
說到一個(gè)成型的刷新頭柿顶,
MJRefresh
它的核心邏輯應(yīng)該是:當(dāng)該header
添加到scrollView
上后九串,作者以scrollView
往下拉動(dòng)到的不同偏移量品山,來相應(yīng)地給header
定義了幾種狀態(tài)state
笆载。scrollView
的偏移量contentOffset
引起header
的state
變化涯呻,而不同state
下要設(shè)置各自的顯示樣式涝登。
我們直接來看MJRefreshHeader.m
文件胀滚。屬性和方法概覽如下圖所示咽笼,因?yàn)樵擃惔a較長(zhǎng)剑刑,我們分段來分析施掏。
可以看到在.m文件的extension
中定義了幾個(gè)屬性:顯示上次刷新時(shí)間的標(biāo)簽updatedTimeLabel
萌腿;顯示狀態(tài)對(duì)應(yīng)文字的標(biāo)簽stateLabel
毁菱;NSDate
類型的,表示上次刷新時(shí)間的updatedTime
窗慎;以及一個(gè)代表所有狀態(tài)對(duì)應(yīng)文字的字典stateTitles
遮斥。
然后.m
文件一開始便實(shí)現(xiàn)了其對(duì)應(yīng)的getter
方法,在getter
方法里直接將其addSubView:
給header
了帆精。
@interface MJRefreshHeader()
/** 顯示上次刷新時(shí)間的標(biāo)簽 */
@property (weak, nonatomic) UILabel *updatedTimeLabel;
/** 上次刷新時(shí)間 */
@property (strong, nonatomic) NSDate *updatedTime;
/** 顯示狀態(tài)文字的標(biāo)簽 */
@property (weak, nonatomic) UILabel *stateLabel;
/** 所有狀態(tài)對(duì)應(yīng)的文字 */
@property (strong, nonatomic) NSMutableDictionary *stateTitles;
@end
@implementation MJRefreshHeader
#pragma mark - 懶加載
- (NSMutableDictionary *)stateTitles
{
if (!_stateTitles) {
self.stateTitles = [NSMutableDictionary dictionary];
}
return _stateTitles;
}
- (UILabel *)stateLabel
{
if (!_stateLabel) {
UILabel *stateLabel = [[UILabel alloc] init];
stateLabel.backgroundColor = [UIColor clearColor];
stateLabel.textAlignment = NSTextAlignmentCenter;
[self addSubview:_stateLabel = stateLabel];
}
return _stateLabel;
}
- (UILabel *)updatedTimeLabel
{
if (!_updatedTimeLabel) {
UILabel *updatedTimeLabel = [[UILabel alloc] init];
updatedTimeLabel.backgroundColor = [UIColor clearColor];
updatedTimeLabel.textAlignment = NSTextAlignmentCenter;
[self addSubview:_updatedTimeLabel = updatedTimeLabel];
}
return _updatedTimeLabel;
}
然后下來是幾個(gè)“初始化方法”较屿,“準(zhǔn)備方法”。在它們幾個(gè)方法中基本都是設(shè)置一些默認(rèn)的屬性卓练。
#pragma mark - 初始化方法
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// 設(shè)置默認(rèn)的dateKey
self.dateKey = MJRefreshHeaderUpdatedTimeKey;
// 設(shè)置為默認(rèn)狀態(tài)
self.state = MJRefreshHeaderStateIdle;
// 初始化文字
[self setTitle:MJRefreshHeaderStateIdleText forState:MJRefreshHeaderStateIdle];
[self setTitle:MJRefreshHeaderStatePullingText forState:MJRefreshHeaderStatePulling];
[self setTitle:MJRefreshHeaderStateRefreshingText forState:MJRefreshHeaderStateRefreshing];
}
return self;
}
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
if (newSuperview) {
self.mj_h = MJRefreshHeaderHeight;
}
}
- (void)drawRect:(CGRect)rect
{
if (self.state == MJRefreshHeaderStateWillRefresh) {
self.state = MJRefreshHeaderStateRefreshing;
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
// 設(shè)置自己的位置
self.mj_y = - self.mj_h;
// 2個(gè)標(biāo)簽都隱藏
if (self.stateHidden && self.updatedTimeHidden) return;
if (self.updatedTimeHidden) { // 顯示狀態(tài)
_stateLabel.frame = self.bounds;
} else if (self.stateHidden) { // 顯示時(shí)間
self.updatedTimeLabel.frame = self.bounds;
} else { // 都顯示
CGFloat stateH = self.mj_h * 0.55;
CGFloat stateW = self.mj_w;
// 1.狀態(tài)標(biāo)簽
_stateLabel.frame = CGRectMake(0, 0, stateW, stateH);
// 2.時(shí)間標(biāo)簽
CGFloat updatedTimeY = stateH;
CGFloat updatedTimeH = self.mj_h - stateH;
CGFloat updatedTimeW = stateW;
self.updatedTimeLabel.frame = CGRectMake(0, updatedTimeY, updatedTimeW, updatedTimeH);
}
}
下面兩個(gè)方法是對(duì)header
里上次刷新時(shí)間的處理隘蝎。一個(gè)dateKey
對(duì)應(yīng)一個(gè)updatedTime
,每個(gè)頁面在刷新過程中只要變?yōu)?code>refreshing狀態(tài)襟企,便會(huì)存儲(chǔ)該時(shí)刻的時(shí)間嘱么,是存儲(chǔ)在userDefault
中的,以dateKey
為鍵整吆,以updatedTime
為值。
- (void)setDateKey:(NSString *)dateKey
{
_dateKey = dateKey ? dateKey : MJRefreshHeaderUpdatedTimeKey;
self.updatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:_dateKey];
}
#pragma mark 設(shè)置最后的更新時(shí)間
- (void)setUpdatedTime:(NSDate *)updatedTime
{
_updatedTime = updatedTime;
if (updatedTime) {
[[NSUserDefaults standardUserDefaults] setObject:updatedTime forKey:self.dateKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
if (self.updatedTimeTitle) {
self.updatedTimeLabel.text = self.updatedTimeTitle(updatedTime);
return;
}
if (updatedTime) {
// 1.獲得年月日
NSCalendar *calendar = [NSCalendar currentCalendar];
NSUInteger unitFlags = NSCalendarUnitYear| NSCalendarUnitMonth | NSCalendarUnitDay |NSCalendarUnitHour |NSCalendarUnitMinute;
NSDateComponents *cmp1 = [calendar components:unitFlags fromDate:updatedTime];
NSDateComponents *cmp2 = [calendar components:unitFlags fromDate:[NSDate date]];
// 2.格式化日期
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
if ([cmp1 day] == [cmp2 day]) { // 今天
formatter.dateFormat = @"今天 HH:mm";
} else if ([cmp1 year] == [cmp2 year]) { // 今年
formatter.dateFormat = @"MM-dd HH:mm";
} else {
formatter.dateFormat = @"yyyy-MM-dd HH:mm";
}
NSString *time = [formatter stringFromDate:updatedTime];
// 3.顯示日期
self.updatedTimeLabel.text = [NSString stringWithFormat:@"最后更新:%@", time];
} else {
self.updatedTimeLabel.text = @"最后更新:無記錄";
}
}
下面就到了最核心的地方了。我們?cè)谏弦黄呀?jīng)說了在MJRefreshComponent
類中已經(jīng)以KVO的方式給scrollView
的contentOffset
屬性添加了監(jiān)聽。只要contentOffset
屬性發(fā)生變化便會(huì)執(zhí)行下面的回調(diào)方法。代碼中注釋得很詳細(xì)了,就不贅述了。
需要說明的是一開始我不明白_scrollViewOriginalInset
這個(gè)變量是什么意思——它就是代表scrollView
的原始contentInset
值。它不應(yīng)當(dāng)是0嗎?其實(shí)有時(shí)不一定是0。
#pragma mark KVO屬性監(jiān)聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// 遇到這些情況就直接返回
if (!self.userInteractionEnabled || self.alpha <= 0.01 || self.hidden || self.state == MJRefreshHeaderStateRefreshing) return;
// 根據(jù)contentOffset調(diào)整state
if ([keyPath isEqualToString:MJRefreshContentOffset]) {
[self adjustStateWithContentOffset];
}
}
#pragma mark 根據(jù)contentOffset調(diào)整state
- (void)adjustStateWithContentOffset
{
if (self.state != MJRefreshHeaderStateRefreshing) {
// 在刷新過程中,跳轉(zhuǎn)到下一個(gè)控制器時(shí)明吩,contentInset可能會(huì)變
_scrollViewOriginalInset = _scrollView.contentInset;
}
// 在刷新的 refreshing 狀態(tài)仍律,動(dòng)態(tài)設(shè)置 content inset
if (self.state == MJRefreshHeaderStateRefreshing ) {
if(_scrollView.contentOffset.y >= -_scrollViewOriginalInset.top ) {
_scrollView.mj_insetT = _scrollViewOriginalInset.top;
} else {
_scrollView.mj_insetT = MIN(_scrollViewOriginalInset.top + self.mj_h,
_scrollViewOriginalInset.top - _scrollView.contentOffset.y);
}
return;
}
// 當(dāng)前的contentOffset
CGFloat offsetY = _scrollView.mj_offsetY;
// 頭部控件剛好出現(xiàn)的offsetY
CGFloat happenOffsetY = - _scrollViewOriginalInset.top;
// 如果是向上滾動(dòng)到看不見頭部控件,直接返回
if (offsetY >= happenOffsetY) return;
// 普通 和 即將刷新 的臨界點(diǎn)
CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
if (_scrollView.isDragging)
{
self.pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
// 剛開始往下拉,拉到偏移量大于54時(shí),狀態(tài)變?yōu)閜ulling
if (self.state == MJRefreshHeaderStateIdle && offsetY < normal2pullingOffsetY) {
// #轉(zhuǎn)為即將刷新狀態(tài)
self.state = MJRefreshHeaderStatePulling;
// #當(dāng)往下拉超過54后,往回推,推到54以上時(shí)狀態(tài)由pulling變?yōu)閕dle
} else if (self.state == MJRefreshHeaderStatePulling && offsetY >= normal2pullingOffsetY) {
// 轉(zhuǎn)為普通狀態(tài)
self.state = MJRefreshHeaderStateIdle;
}
}
// #以下為松開手后
// 若松開手時(shí)此刻的狀態(tài)是pulling,說明已往下拉過54的偏移量凭语,則將其變?yōu)閞efreshing狀態(tài)
else if (self.state == MJRefreshHeaderStatePulling) {// 即將刷新 && 手松開
self.pullingPercent = 1.0;
// 開始刷新
self.state = MJRefreshHeaderStateRefreshing;
} else {
self.pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
}
}
接下來就是幾個(gè)提供給外部控制刷新狀態(tài)的方法了。除了控件自身可以通過contentOffset
來切換狀態(tài)外偶器,外部調(diào)用者也可以調(diào)用這幾個(gè)方法來切換header
的狀態(tài)。包括最后一個(gè)方法判斷header
是否正在刷新,即判斷它當(dāng)前的狀態(tài)是否為MJRefreshHeaderStateRefreshing
。
- (void)beginRefreshing
{
if (self.window) {
self.state = MJRefreshHeaderStateRefreshing;
} else {
self.state = MJRefreshHeaderStateWillRefresh;
// 刷新(預(yù)防從另一個(gè)控制器回到這個(gè)控制器的情況港庄,回來要重新刷新一下)
[self setNeedsDisplay];
}
}
- (void)endRefreshing
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.state = MJRefreshHeaderStateIdle;
});
}
- (BOOL)isRefreshing
{
return self.state == MJRefreshHeaderStateRefreshing;
}
接下來也是一個(gè)非常核心的方法,重寫了state
的setter
方法。我們看看其在切換state
時(shí)都做了什么事缀踪。
第一個(gè)case
的意思是,外部調(diào)用了endRefreshing
方法蔼紧,停止了刷新湖蜕,狀態(tài)由refreshing變?yōu)閕dle熙含。此時(shí)首先記錄并存儲(chǔ)了當(dāng)前的存儲(chǔ)時(shí)間或粮,然后將header
從頂部退出:其實(shí)是將scrollView
的contenInset
由原來的54設(shè)置為0冗尤。
第二個(gè)case
的意思是毛雇,開始刷新了虽缕,在開始refreshing
狀態(tài)時(shí),首先將header
的contentOffset
和contentInset
的值均設(shè)置為54。然后有block
回調(diào)灼擂,就調(diào)用執(zhí)行block
回調(diào)语御,有SEL
回調(diào)骨田,就調(diào)用執(zhí)行SEL
回調(diào)殖氏。
- (void)setState:(MJRefreshHeaderState)state
{
if (_state == state) return;
// 舊狀態(tài)
MJRefreshHeaderState oldState = _state;
// 賦值
_state = state;
// 設(shè)置狀態(tài)文字
_stateLabel.text = _stateTitles[@(state)];
switch (state) {
case MJRefreshHeaderStateIdle: {
if (oldState == MJRefreshHeaderStateRefreshing) { // #當(dāng)外部調(diào)用endRefreshing后,由refreshing狀態(tài)變?yōu)閕dle狀態(tài)
// 保存刷新時(shí)間
self.updatedTime = [NSDate date];
// 恢復(fù)inset和offset
[UIView animateWithDuration:MJRefreshSlowAnimationDuration delay:0.0 options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState animations:^{
// 修復(fù)top值不斷累加
_scrollView.mj_insetT -= self.mj_h; // 刷新的header視圖從頂部退出:其實(shí)是將scrollView的contenInset由原來的54設(shè)置為0
} completion:nil];
}
break;
}
case MJRefreshHeaderStateRefreshing: {
[UIView animateWithDuration:MJRefreshFastAnimationDuration delay:0.0 options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState animations:^{
// 增加滾動(dòng)區(qū)域
CGFloat top = _scrollViewOriginalInset.top + self.mj_h;
_scrollView.mj_insetT = top;
// 設(shè)置滾動(dòng)位置
_scrollView.mj_offsetY = - top;
} completion:^(BOOL finished) {
// 回調(diào)
if (self.refreshingBlock) {
self.refreshingBlock();
}
if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
msgSend(msgTarget(self.refreshingTarget), self.refreshingAction, self);
}
}];
break;
}
default:
break;
}
}
在該類的最后是下面幾個(gè)功能方法蛉签。前兩個(gè)是重寫的父類的方法胡陪,后兩個(gè)是重寫的本類的兩個(gè)屬性的setter
方法,用來控制stateLabel
和updatedTimeLabel
的可見性碍舍,因?yàn)檫@兩個(gè)可不可見會(huì)影響UI布局柠座,所以在倆方法內(nèi)都調(diào)用了setNeedsLayout
方法表示需要重繪,會(huì)再次執(zhí)行一遍layoutSubviews
方法片橡,重新調(diào)整一遍布局妈经。
- (void)setTextColor:(UIColor *)textColor
{
[super setTextColor:textColor];
self.updatedTimeLabel.textColor = textColor;
self.stateLabel.textColor = textColor;
}
- (void)setFont:(UIFont *)font
{
[super setFont:font];
self.updatedTimeLabel.font = font;
self.stateLabel.font = font;
}
- (void)setStateHidden:(BOOL)stateHidden
{
_stateHidden = stateHidden;
self.stateLabel.hidden = stateHidden;
[self setNeedsLayout];
}
- (void)setUpdatedTimeHidden:(BOOL)updatedTimeHidden
{
_updatedTimeHidden = updatedTimeHidden;
self.updatedTimeLabel.hidden = updatedTimeHidden;
[self setNeedsLayout];
}
結(jié)尾
至此,MJRefresh
源碼的邏輯基本梳理清楚了锻全,能看清它是怎么實(shí)現(xiàn)的了狂塘。后面還會(huì)寫一篇,整理一下它里面出現(xiàn)的一些值得掌握的知識(shí)點(diǎn)鳄厌。