MJRefresh源码阅读2——核心类MJRefreshHead

作者: Wang66 | 来源:发表于2017-01-04 15:21 被阅读274次

    前言

    MJRefresh源码阅读1——结构梳理中我们已经说了MJRefreshHeader是整个控件的核心类,它完成了一个刷新控件应该有的所有逻辑和UI显示,它已经是个成型的,较简单的,麻雀虽小五脏俱全的刷新头header

    说到一个成型的刷新头,MJRefresh它的核心逻辑应该是:当该header添加到scrollView上后,作者以scrollView往下拉动到的不同偏移量,来相应地给header定义了几种状态statescrollView的偏移量contentOffset引起headerstate变化,而不同state下要设置各自的显示样式。

    我们直接来看MJRefreshHeader.m文件。属性和方法概览如下图所示,因为该类代码较长,我们分段来分析。

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

    可以看到在.m文件的extension中定义了几个属性:显示上次刷新时间的标签updatedTimeLabel;显示状态对应文字的标签stateLabelNSDate类型的,表示上次刷新时间的updatedTime;以及一个代表所有状态对应文字的字典stateTitles

    然后.m文件一开始便实现了其对应的getter方法,在getter方法里直接将其addSubView:header了。

    @interface MJRefreshHeader()
    /** 显示上次刷新时间的标签 */
    @property (weak, nonatomic) UILabel *updatedTimeLabel;
    /** 上次刷新时间 */
    @property (strong, nonatomic) NSDate *updatedTime;
    /** 显示状态文字的标签 */
    @property (weak, nonatomic) UILabel *stateLabel;
    /** 所有状态对应的文字 */
    @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;
    }
    

    然后下来是几个“初始化方法”,“准备方法”。在它们几个方法中基本都是设置一些默认的属性。

    #pragma mark - 初始化方法
    - (instancetype)initWithFrame:(CGRect)frame {
        if (self = [super initWithFrame:frame]) {
            // 设置默认的dateKey
            self.dateKey = MJRefreshHeaderUpdatedTimeKey;
            
            // 设置为默认状态
            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];
        
        // 设置自己的位置
        self.mj_y = - self.mj_h;
        
        // 2个标签都隐藏
        if (self.stateHidden && self.updatedTimeHidden) return;
        
        if (self.updatedTimeHidden) { // 显示状态
            _stateLabel.frame = self.bounds;
        } else if (self.stateHidden) { // 显示时间
            self.updatedTimeLabel.frame = self.bounds;
        } else { // 都显示
            CGFloat stateH = self.mj_h * 0.55;
            CGFloat stateW = self.mj_w;
            // 1.状态标签
            _stateLabel.frame = CGRectMake(0, 0, stateW, stateH);
            
            // 2.时间标签
            CGFloat updatedTimeY = stateH;
            CGFloat updatedTimeH = self.mj_h - stateH;
            CGFloat updatedTimeW = stateW;
            self.updatedTimeLabel.frame = CGRectMake(0, updatedTimeY, updatedTimeW, updatedTimeH);
        }
    }
    

    下面两个方法是对header里上次刷新时间的处理。一个dateKey对应一个updatedTime,每个页面在刷新过程中只要变为refreshing状态,便会存储该时刻的时间,是存储在userDefault中的,以dateKey为键,以updatedTime为值。

    - (void)setDateKey:(NSString *)dateKey
    {
        _dateKey = dateKey ? dateKey : MJRefreshHeaderUpdatedTimeKey;
        
        self.updatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:_dateKey];
    }
    
    #pragma mark 设置最后的更新时间
    - (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 = @"最后更新:无记录";
        }
    }
    

    下面就到了最核心的地方了。我们在上一篇已经说了在MJRefreshComponent类中已经以KVO的方式给scrollViewcontentOffset属性添加了监听。只要contentOffset属性发生变化便会执行下面的回调方法。代码中注释得很详细了,就不赘述了。
    需要说明的是一开始我不明白_scrollViewOriginalInset这个变量是什么意思——它就是代表scrollView的原始contentInset值。它不应当是0吗?其实有时不一定是0。

    #pragma mark KVO属性监听
    - (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;
        
        // 根据contentOffset调整state
        if ([keyPath isEqualToString:MJRefreshContentOffset]) {
            [self adjustStateWithContentOffset];
        }
    }
    
    #pragma mark 根据contentOffset调整state
    - (void)adjustStateWithContentOffset
    {
        if (self.state != MJRefreshHeaderStateRefreshing) {
            // 在刷新过程中,跳转到下一个控制器时,contentInset可能会变
            _scrollViewOriginalInset = _scrollView.contentInset;
        }
        
        // 在刷新的 refreshing 状态,动态设置 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;
        }
        
        // 当前的contentOffset
        CGFloat offsetY = _scrollView.mj_offsetY;
        // 头部控件刚好出现的offsetY
        CGFloat happenOffsetY = - _scrollViewOriginalInset.top;
        
        // 如果是向上滚动到看不见头部控件,直接返回
        if (offsetY >= happenOffsetY) return;
        
        // 普通 和 即将刷新 的临界点
        CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
        if (_scrollView.isDragging)
        {
            self.pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
            // 刚开始往下拉,拉到偏移量大于54时,状态变为pulling
            if (self.state == MJRefreshHeaderStateIdle && offsetY < normal2pullingOffsetY) {
                // #转为即将刷新状态
                self.state = MJRefreshHeaderStatePulling;
                // #当往下拉超过54后,往回推,推到54以上时状态由pulling变为idle
            } else if (self.state == MJRefreshHeaderStatePulling && offsetY >= normal2pullingOffsetY) {
                // 转为普通状态
                self.state = MJRefreshHeaderStateIdle;
            }
        }
        // #以下为松开手后
        // 若松开手时此刻的状态是pulling,说明已往下拉过54的偏移量,则将其变为refreshing状态
        else if (self.state == MJRefreshHeaderStatePulling) {// 即将刷新 && 手松开
            self.pullingPercent = 1.0;
            // 开始刷新
            self.state = MJRefreshHeaderStateRefreshing;
        } else {
            self.pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
        }
    }
    

    接下来就是几个提供给外部控制刷新状态的方法了。除了控件自身可以通过contentOffset来切换状态外,外部调用者也可以调用这几个方法来切换header的状态。包括最后一个方法判断header是否正在刷新,即判断它当前的状态是否为MJRefreshHeaderStateRefreshing

    - (void)beginRefreshing
    {
        if (self.window) {
            self.state = MJRefreshHeaderStateRefreshing;
        } else {
            self.state = MJRefreshHeaderStateWillRefresh;
            // 刷新(预防从另一个控制器回到这个控制器的情况,回来要重新刷新一下)
            [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;
    }
    

    接下来也是一个非常核心的方法,重写了statesetter方法。我们看看其在切换state时都做了什么事。

    第一个case的意思是,外部调用了endRefreshing方法,停止了刷新,状态由refreshing变为idle。此时首先记录并存储了当前的存储时间,然后将header从顶部退出:其实是将scrollViewcontenInset由原来的54设置为0。
    第二个case的意思是,开始刷新了,在开始refreshing状态时,首先将headercontentOffsetcontentInset的值均设置为54。然后有block回调,就调用执行block回调,有SEL回调,就调用执行SEL回调。

    - (void)setState:(MJRefreshHeaderState)state
    {
        if (_state == state) return;
        
        // 旧状态
        MJRefreshHeaderState oldState = _state;
        
        // 赋值
        _state = state;
        
        // 设置状态文字
        _stateLabel.text = _stateTitles[@(state)];
        
        switch (state) {
            case MJRefreshHeaderStateIdle: {
                if (oldState == MJRefreshHeaderStateRefreshing) { // #当外部调用endRefreshing后,由refreshing状态变为idle状态
                    // 保存刷新时间
                    self.updatedTime = [NSDate date];
                    
                    // 恢复inset和offset
                    [UIView animateWithDuration:MJRefreshSlowAnimationDuration delay:0.0 options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState animations:^{
                        // 修复top值不断累加
                        _scrollView.mj_insetT -= self.mj_h; // 刷新的header视图从顶部退出:其实是将scrollView的contenInset由原来的54设置为0
                    } completion:nil];
                }
                break;
            }
                
            case MJRefreshHeaderStateRefreshing: {
                [UIView animateWithDuration:MJRefreshFastAnimationDuration delay:0.0 options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState animations:^{
                    // 增加滚动区域
                    CGFloat top = _scrollViewOriginalInset.top + self.mj_h;
                    _scrollView.mj_insetT = top;
                    
                    // 设置滚动位置
                    _scrollView.mj_offsetY = - top;
                } completion:^(BOOL finished) {
                    // 回调
                    if (self.refreshingBlock) {
                        self.refreshingBlock();
                    }
                    
                    if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
                        msgSend(msgTarget(self.refreshingTarget), self.refreshingAction, self);
                    }
                }];
                break;
            }
                
            default:
                break;
        }
    }
    

    在该类的最后是下面几个功能方法。前两个是重写的父类的方法,后两个是重写的本类的两个属性的setter方法,用来控制stateLabelupdatedTimeLabel的可见性,因为这两个可不可见会影响UI布局,所以在俩方法内都调用了setNeedsLayout方法表示需要重绘,会再次执行一遍layoutSubviews方法,重新调整一遍布局。

    - (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];
    }
    

    结尾

    至此,MJRefresh源码的逻辑基本梳理清楚了,能看清它是怎么实现的了。后面还会写一篇,整理一下它里面出现的一些值得掌握的知识点。

    相关文章

      网友评论

        本文标题:MJRefresh源码阅读2——核心类MJRefreshHead

        本文链接:https://www.haomeiwen.com/subject/nurovttx.html