MJRefresh源碼閱讀2——核心類MJRefreshHeader

前言

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引起headerstate變化涯呻,而不同state下要設(shè)置各自的顯示樣式涝登。

我們直接來看MJRefreshHeader.m文件胀滚。屬性和方法概覽如下圖所示咽笼,因?yàn)樵擃惔a較長(zhǎng)剑刑,我們分段來分析施掏。

屏幕快照 2017-01-04 下午1.47.05.png

可以看到在.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的方式給scrollViewcontentOffset屬性添加了監(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è)非常核心的方法,重寫了statesetter方法。我們看看其在切換state時(shí)都做了什么事缀踪。

第一個(gè)case的意思是,外部調(diào)用了endRefreshing方法蔼紧,停止了刷新湖蜕,狀態(tài)由refreshing變?yōu)閕dle熙含。此時(shí)首先記錄并存儲(chǔ)了當(dāng)前的存儲(chǔ)時(shí)間或粮,然后將header從頂部退出:其實(shí)是將scrollViewcontenInset由原來的54設(shè)置為0冗尤。
第二個(gè)case的意思是毛雇,開始刷新了虽缕,在開始refreshing狀態(tài)時(shí),首先將headercontentOffsetcontentInset的值均設(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方法,用來控制stateLabelupdatedTimeLabel的可見性碍舍,因?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)鳄厌。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末荞胡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子了嚎,更是在濱河造成了極大的恐慌泪漂,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件歪泳,死亡現(xiàn)場(chǎng)離奇詭異萝勤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)呐伞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門敌卓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人伶氢,你說我怎么就攤上這事趟径”窭簦” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵蜗巧,是天一觀的道長(zhǎng)掌眠。 經(jīng)常有香客問我,道長(zhǎng)幕屹,這世上最難降的妖魔是什么蓝丙? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮望拖,結(jié)果婚禮上渺尘,老公的妹妹穿的比我還像新娘。我一直安慰自己靠娱,他們只是感情好沧烈,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著像云,像睡著了一般锌雀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上迅诬,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天腋逆,我揣著相機(jī)與錄音,去河邊找鬼侈贷。 笑死惩歉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的俏蛮。 我是一名探鬼主播撑蚌,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼搏屑!你這毒婦竟也來了争涌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤辣恋,失蹤者是張志新(化名)和其女友劉穎亮垫,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伟骨,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡饮潦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了携狭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片继蜡。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出稀并,到底是詐尸還是另有隱情鲫剿,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布稻轨,位于F島的核電站,受9級(jí)特大地震影響雕凹,放射性物質(zhì)發(fā)生泄漏殴俱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一枚抵、第九天 我趴在偏房一處隱蔽的房頂上張望线欲。 院中可真熱鬧,春花似錦汽摹、人聲如沸李丰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽趴泌。三九已至,卻和暖如春拉庶,著一層夾襖步出監(jiān)牢的瞬間嗜憔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工氏仗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吉捶,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓皆尔,卻偏偏與公主長(zhǎng)得像呐舔,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子慷蠕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容