美文网首页
MJRefresh源码分析 下拉刷新

MJRefresh源码分析 下拉刷新

作者: 宋鸿康iOS | 来源:发表于2018-01-29 14:02 被阅读54次

    MJRefresh是李明杰老师的一个开源项目,GitHub目前已经有10000多star,GitHub地址是MJRefresh
    下面我们一起来分析下MJRefresh框架的实现过程。

    • MJRefresh中类与类之间的联系


      mjrefresh.png
    • 从我们使用MJRefresh框架的调用代码分析
      eg:
     self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
       // 属性中的回调
        }];
     [self.tableView.mj_header beginRefreshing];
    

    上面的代码会调用MJRefreshNormalHeader父类MJRefreshStateHeader的父类MJRefreshHeader的方法:

    + (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
    {
      // 实例化MJRefreshHeader的对象
        MJRefreshHeader *cmp = [[self alloc] init];
        // refreshingBlock 父类的属性,把refreshingBlock赋值cmp.refreshingBlock属性
        cmp.refreshingBlock = refreshingBlock;
        return cmp;
    }
    

    上面的headerWithRefreshingBlock:refreshingBlock;方法实例化一个一个对象cmp,会触发MJRefreshHeader父类中的- (instancetype)initWithFrame:(CGRect)frame的方法。

    #pragma mark - 初始化方法
    - (instancetype)initWithFrame:(CGRect)frame
    {
      // 注意,此时的self 是 MJRefreshNormalHeader的对象,为什么是 MJRefreshNormalHeader的对象,设计到继承的知识点,可以具体参考继承,这里就不过多的说明
        if (self = [super initWithFrame:frame]) {
            //  调用 MJRefreshNormalHeader 中prepare方法
            [self prepare];
             // 默认是普通状态,调用MJRefreshNormalHeadersetState方法
            self.state = MJRefreshStateIdle;
        }
        return self;
    }
    

    我们回到MJRefreshNormalHeader类中的prepare方法,方法具体实现如下

    #pragma mark - 重写父类的方法
    - (void)prepare
    {
        // 调用父类的 prepare 父类 是 MJRefreshStateHeader
        [super prepare];
        // 设置菊花样式
        self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
    }
    

    此时又会去调用MJRefreshNormalHeader父类MJRefreshStateHeader中的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];
    }
    

    然后又会去调用父类中的prepare的方法,直到MJRefreshComponent类中的prepare的方法执行完毕。关于prepare方法,里面都是做一些初始化和frame的设置,比较简单,就不具体分析了。

    再回到最开始的方法

    self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
      // 属性中的回调
       }];
    

    MJRefreshNormalHeader视图赋值给mj_headermj_headerUIScrollView+MJRefresh类中的属性,要给分类添加属性,就要用到runtime机制,具体代码如下:

    #pragma mark - header
    static const char MJRefreshHeaderKey = '\0';
    - (void)setMj_header:(MJRefreshHeader *)mj_header
    {
        if (mj_header != self.mj_header) {
            // 删除旧的,添加新的
            [self.mj_header removeFromSuperview];
            // A insertSubView B AtIndex:2 是将B插入到A的子视图index为2的位置(最底下是0)
            // eg [self addsuview: mj_header];
            [self insertSubview:mj_header atIndex:0];
            // 手动kvo
            [self willChangeValueForKey:@"mj_header"]; // KVO
            // 给分类中的属性添加一个set方法....
            // 分类能添加属性。但是不会自己生成getter和setter方法
            objc_setAssociatedObject(self, &MJRefreshHeaderKey,
                                     mj_header, OBJC_ASSOCIATION_ASSIGN);
            // 手动kvo
            [self didChangeValueForKey:@"mj_header"]; // KVO
        }
    }
    // get方法
    - (MJRefreshHeader *)mj_header
    {
        return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
    }
    

    setMj_header的方法中,监听属性用了iOS 的设计模式 KVO

     [self willChangeValueForKey:@"mj_header"]; // KVO
     [self didChangeValueForKey:@"mj_header"]; // KVO
    

    为什么要用 willChangeValueForKeydidChangeValueForKey方法监听分类的中的属性,而不是- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;具体可以参考KVO的在分类中的用法

    [self insertSubview:mj_header atIndex:0]; A insertSubView B AtIndex:0是将B插入到A的子视图index为0的位置。

    1、这句代码会触发MJRefreshComponent类中的- (void)willMoveToSuperview:(nullable UIView *)newSuperview;此方法什么时候被调用?经过查资料得知:当视图即将加入父视图时或者当视图即将从父视图移除时调用,具体我们分析下此方法

    // newSuperview 就是父视图 这里值得 uiscrollerView
    - (void)willMoveToSuperview:(UIView *)newSuperview
    {
        [super willMoveToSuperview:newSuperview];
        // 如果不是UIScrollView,不做任何事情
        if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
        // 旧的父控件移除监听
        [self removeObservers];
        if (newSuperview) { // 新的父控件
            // 设置宽度
            self.mj_w = newSuperview.mj_w;
            // 设置位置
            self.mj_x = 0;
            // 记录UIScrollView
            _scrollView = (UIScrollView *)newSuperview;
            // 设置永远支持垂直弹簧效果
            _scrollView.alwaysBounceVertical = YES;
            // 记录UIScrollView最开始的contentInset
            _scrollViewOriginalInset = _scrollView.contentInset;
           ;
            NSLog(@"contentInset:%@",NSStringFromUIEdgeInsets(_scrollView.contentInset));
            // 添加监听
            [self addObservers];
        }
    }
    

    此方法中的 self.mj_w = newSuperview.mj_w; self就是下拉的展示出来的viewmj_wUIView+MJExtension中的属性,实现的set的方法- (void)setMj_w:(CGFloat)mj_w,具体方法实现如下

    - (void)setMj_w:(CGFloat)mj_w
    {
        CGRect frame = self.frame;
        frame.size.width = mj_w;
        self.frame = frame;
    }
    

    分析到这里,应该明白了self.mj_w = newSuperview.mj_w;的意思了。self.mj_w = CGRectMake(original, original, newSuperview.mj_w, original);

    [self addObservers];用KVO添加监听,给当前的UIScrollView添加了contentOffsetcontentSizepanGestureRecognizer 的监听

    2、 [self insertSubview:mj_header atIndex:0];此方法还会触发MJRefreshComponent 类中layoutSubviews方法,触发 layoutSubviews 有哪些操作?
    找了下资料并总结下:
    1、调用 addSubview 方法时会执行该方法
    2、设置并改变视图的frame属性时会触发该方法
    3、滑动UIScrollView及继承与UIScrollView的控件时会触发该方法
    4、旋转屏幕时,会触发父视图的layoutSubviews方法、设置并改变视图的frame属性时会触发父视图的layoutSubviews方法

    OK,咱们一起看看MJRefreshComponent类中的layoutSubviews方法

    - (void)layoutSubviews
    {
    // 此处的self依然是MJRefreshNormalHeader的对象
        [self placeSubviews];
        [super layoutSubviews];
    }
    

    MJRefreshNormalHeader类中的placeSubviews 添加了两个视图arrowView(箭头视图)、loadingView(菊花视图)
    MJRefreshStateHeader类中的placeSubviews 添加了两个视图stateLabel(状态label )、lastUpdatedTimeLabel(显示时间label)
    MJRefreshHeader类中的placeSubviews 添加了设置了当前视图的Y坐标
    MJRefreshComponent类中的placeSubviews 没有干啥 ☺
    鉴于placeSubviews方法比较简单,都是关于界面的搭建,再次就不多多啰嗦了。

    OK,分析到这里界面啥的都出来了。下面具体分析下拉的视图如何出现
    由于监听了UIScrollViewcontentOffset属性,当我们下拉的时候,触发监听方法。监听方法在MJRefreshHeader类中

    - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
    {
        [super scrollViewContentOffsetDidChange:change];
        
        
        // 在刷新的refreshing状态
        if (self.state == MJRefreshStateRefreshing) {
            if (self.window == nil) return;
            
            NSLog(@"%@",NSStringFromCGPoint(self.scrollView.contentOffset));
            // sectionheader停留解决
            
            //- self.scrollView.mj_offsetY:-(-54)= 54 : 刷新的时候,偏移量是不动的。偏移量 = 状态栏 + 导航栏 + header的高度
            //_scrollViewOriginalInset.top:64 (状态栏 + 导航栏)
            //insetT 取二者之间大的那一个
            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;
        }
        NSLog(@"scrollViewContentOffsetDidChange");
        // 跳转到下一个控制器时,contentInset可能会变
         _scrollViewOriginalInset = self.scrollView.contentInset;
        // 当前的contentOffset  Y
        CGFloat offsetY = self.scrollView.mj_offsetY;
        // 头部控件刚好出现的offsetY
        CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
        
        // 如果是向上滚动到看不见头部控件,直接返回
        // >= -> >
        // 解释下: offsetY    正值 就是上滑动
        //         offsetY   负值  就是下拉
         if (offsetY > happenOffsetY) return;
        
        // 从普通 到 即将刷新 的临界距离 normal2pullingOffsetY = -54
        CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
        
        //下拉的百分比:下拉的距离与header高度的比值
        CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
        
        if (self.scrollView.isDragging) { // 如果正在拖拽
            self.pullingPercent = pullingPercent;
            
            if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
            // 如果当前为默认状态 && 下拉的距离大于临界距离(将tableview下拉得很低),则将状态切换为可以刷新
                self.state = MJRefreshStatePulling;
            } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
                // 转为普通状态
                self.state = MJRefreshStateIdle;
            }
        }
        
        else if (self.state == MJRefreshStatePulling) {// 即将刷新 && 手松开
            // 开始刷新
            [self beginRefreshing];
        }
        else if (pullingPercent < 1) {
                    self.pullingPercent = pullingPercent;
        }
    }
    
    

    根据不同的state展示界面
    MJRefreshStateHeader中的setState方法

    - (void)setState:(MJRefreshState)state
    {
    //    MJRefreshCheckState
        // 状态检查
    //#define MJRefreshCheckState \
        
        MJRefreshState oldState = self.state;
        if (state == oldState) return; 
        [super setState:state];
        // 设置状态文字
        self.stateLabel.text = self.stateTitles[@(state)];
        // 重新设置key(重新显示时间)
        self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
    }
    

    MJRefreshNormalHeader中的setState方法

    - (void)setState:(MJRefreshState)state
    {
    
        MJRefreshState oldState = self.state;
        if (state == oldState) return;
        [super setState:state];
        
        // 根据状态做事情
        if (state == MJRefreshStateIdle) {
            
            if (oldState == MJRefreshStateRefreshing) {
                // 现在的状态是 MJRefreshStateIdle ,上一个状态时 MJRefreshStateRefreshing
                self.arrowView.transform = CGAffineTransformIdentity;
                
                [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                    self.loadingView.alpha = 0.0;
                } completion:^(BOOL finished) {
                    // 如果执行完动画发现不是idle状态,就直接返回,进入其他状态
                    if (self.state != MJRefreshStateIdle) return;
                    
                    self.loadingView.alpha = 1.0;
                    [self.loadingView stopAnimating];
                    self.arrowView.hidden = NO;
                }];
            } else {
                // 当它停止的时候,菊花视图就会自动隐藏。
    //             loadingView.hidesWhenStopped = YES;
                [self.loadingView stopAnimating];
                self.arrowView.hidden = NO;
                [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                    self.arrowView.transform = CGAffineTransformIdentity;
                }];
            }
        } else if (state == MJRefreshStatePulling) {
            // loadingView 就是菊花的视图
            [self.loadingView stopAnimating];
            // 箭头视图
            self.arrowView.hidden = NO;
            // 让箭头旋转180°
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
            }];
            
        }
        
        else if (state == MJRefreshStateRefreshing) {
            self.loadingView.alpha = 1.0; // 防止refreshing -> idle的动画完毕动作没有被执行
            [self.loadingView startAnimating];
            self.arrowView.hidden = YES;
        }
    }
    

    MJRefreshStateHeader中的setState方法

    - (void)setState:(MJRefreshState)state
    {
    //    MJRefreshCheckState
        // 状态检查
    //#define MJRefreshCheckState \
        
        MJRefreshState oldState = self.state;
        if (state == oldState) return; 
        [super setState:state];
        // 设置状态文字
        self.stateLabel.text = self.stateTitles[@(state)];
        // 重新设置key(重新显示时间)
        self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
    }
    

    MJRefreshNormalHeader中的setState方法

    - (void)setState:(MJRefreshState)state
    {
        // MJRefreshCheckState
        // 状态检查
        //#define MJRefreshCheckState \
        
        MJRefreshState oldState = self.state;
        if (state == oldState) return;
        [super setState:state];
    
        // 根据状态做事情
        if (state == MJRefreshStateIdle) {
            if (oldState != MJRefreshStateRefreshing) return;
            // 当前的状态必须是 MJRefreshStateIdle ,上一个状态是 MJRefreshStateRefreshing,才可以保存时间和恢复uiscrollerView的 inset和 offset
            
            // 保存刷新时间
            [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
            [[NSUserDefaults standardUserDefaults] synchronize];
            NSLog(@"MJRefreshState");
            // 恢复inset和offset
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                self.scrollView.mj_insetT += self.insetTDelta;
                NSLog(@"%@",NSStringFromUIEdgeInsets(self.scrollView.contentInset));
                // 自动调整透明度
                if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
            } completion:^(BOOL finished) {
                self.pullingPercent = 0.0;
                  if (self.endRefreshingCompletionBlock) {
                    self.endRefreshingCompletionBlock();
                }
            }];
        }
        else if (state == MJRefreshStateRefreshing) {
            // 对UI的调度,都应该在主线程中
             dispatch_async(dispatch_get_main_queue(), ^{
                [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                    CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
                    // 增加滚动区域top
                    self.scrollView.mj_insetT = top;
                    // 设置滚动位置
                    [self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
                } completion:^(BOOL finished) {
                    [self executeRefreshingCallback];
                }];
             });
        }
    }
    
    

    MJRefreshComponent中的setState方法

    - (void)setState:(MJRefreshState)state
    {
        _state = state;
        // 加入主队列的目的是等setState:方法调用完毕、设置完文字后再去布局子控件
        dispatch_async(dispatch_get_main_queue(), ^{
            [self setNeedsLayout];
        });
    }
    

    关于下拉刷新,分析就到此为止,更多用法,参考MJRefreshDemo

    相关文章

      网友评论

          本文标题:MJRefresh源码分析 下拉刷新

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