MJRefresher源码解读 - CrusherWu/study GitHub Wiki

MJRefresher源码解读

1.整体结构

框架图

MJRefreshComponent 做基本的设置(MJRefreshHeader & MJRefreshFooter 都会使用到的属性和办法),让 MJRefreshHeader 和 MJRefreshFooter 分别继承 MJRefreshComponent 的办法来实现下拉和上拉加载的功能。

2.上拉加载部分

上拉加载分两种情况:auto(上拉到一定位置,自动加载) 和 back(需要拉动到一定位置,手释放后才加载数据),直接来看两种效果图。

2.1auto

auto

2.2back

back类型

3.详解back类型的footer

MJRefreshFooter 继承自 MJRefreshComponent;这里我们以 MJRefreshBackNormalFooter 为例子来跟踪一下 MJRefreshFooter 从初始化到处理各个状态的变化。

3.1初始化

初始化的时候我们最关注的无非是控件的frame,因为这关系到控件是否显示在屏幕上和显示在屏幕哪里?所以接下来就通过一系列办法来确认footer的frame。

- (void)example18{
    [self example01];
    
    // 设置回调(一旦进入刷新状态,就调用target的action,也就是调用self的loadMoreData方法)
    self.tableView.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreData)];
    // 设置了底部inset
    self.tableView.contentInset = UIEdgeInsetsMake(0, 0, 30, 0);
    // 忽略掉底部inset
    self.tableView.mj_footer.ignoredScrollViewContentInsetBottom = 30;
}

首先看看 footerWithRefreshingTarget:refreshingAction:办法做了哪些事情?

+ (instancetype)footerWithRefreshingTarget:(id)target refreshingAction:(SEL)action{
    MJRefreshFooter *cmp = [[self alloc] init];//初始化一个self isa->MJRefreshBackNormalFooter
    [cmp setRefreshingTarget:target refreshingAction:action];
    return cmp;
}

到底 [[self alloc] init]做了什么?

self是MJRefreshBackNormalFooter类型,MJRefreshBackNormalFooter 继承自 MJRefreshBackStateFooter ,MJRefreshBackStateFooter 继承 MJRefreshBackFooter,MJRefreshBackFooter继承 MJRefreshFooter,MJRefreshFooter 继承 MJRefreshComponent。根据OC继承原则,先来看看MJRefreshComponent提供的初始化:

  • MJRefreshComponent:
- (instancetype)initWithFrame:(CGRect)frame{
    if (self = [super initWithFrame:frame]) {
        // 准备工作
        [self prepare];
        
        // 默认是普通状态
        self.state = MJRefreshStateIdle;
    }
    return self;
}

- (void)prepare{
    // 基本属性
    self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    self.backgroundColor = [UIColor clearColor];
}

MJRefreshComponent 基类办法中调用了 prepare ,那么根据上述的顺序,一层层追溯一下每个不同的footer子类 在prepare办法中做了那些事情!

  • MJRefreshFooter:
- (void)prepare{
    [super prepare];
    
    // 设置自己的高度
    self.mj_h = MJRefreshFooterHeight;
    
    // 默认不会自动隐藏
    self.automaticallyHidden = NO;
}

footer的高就是在这个办法中确定,现在footer的fame的H已经确认了。接下来继续看 MJRefreshBackFooter 类

  • MJRefreshBackFooter 没有实现 prepare,继续查看 MJRefreshBackStateFooter
- (void)prepare{
    [super prepare];
    
    // 初始化间距
    self.labelLeftInset = MJRefreshLabelLeftInset;
    
    // 初始化文字
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshBackFooterIdleText] forState:MJRefreshStateIdle];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshBackFooterPullingText] forState:MJRefreshStatePulling];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshBackFooterRefreshingText] forState:MJRefreshStateRefreshing];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshBackFooterNoMoreDataText] forState:MJRefreshStateNoMoreData];
}

这里没有实现frame任何信息,只是为footer添加该类型footer需要的子控件。继续查看MJRefreshBackNormalFooter。

  • MJRefreshBackNormalFooter
- (void)prepare{
    [super prepare];
    
    self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
}

MJRefreshBackNormalFooter 同样没有任何确定frame的信息,只是自定义了自己需要的子控件。

已经看完了所有相关类的初始化办法了,目前能确认的只有H。self.mj_h = MJRefreshFooterHeight;那么我们可以在工程中搜索self.mj_w,self.mj_x,self.mj_y;

self.mj_w

通过搜索整个工程,发现只有在 MJRefreshComponent 中有设置 self.mj_w;

- (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 = -_scrollView.mj_insetL;
        
        // 记录UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // 设置永远支持垂直弹簧效果
        _scrollView.alwaysBounceVertical = YES;
        // 记录UIScrollView最开始的contentInset
        _scrollViewOriginalInset = _scrollView.mj_inset;
        
        // 添加监听
        [self addObservers];
    }
}

该办法同时也设置了 self.mj_x,那么只剩下 self.mj_y了。

self.mj_y

通过搜索结果可以看出,有好几处设置了self.mj_y,来看看MJRefreshBackFooter:

- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{
    [super scrollViewContentSizeDidChange:change];
    
    // 内容的高度
    CGFloat contentHeight = self.scrollView.mj_contentH + self.ignoredScrollViewContentInsetBottom;
    // 表格的高度
    CGFloat scrollHeight = self.scrollView.mj_h - self.scrollViewOriginalInset.top - self.scrollViewOriginalInset.bottom + self.ignoredScrollViewContentInsetBottom;
    // 设置位置和尺寸
    self.mj_y = MAX(contentHeight, scrollHeight);
}

MJRefreshBackFooter,在每次UIScrollView的contentSize发现变化的时候(sahng)通过改变Y值来改变控件的位置。

3.2 add到superView
- (void)setMj_footer:(MJRefreshFooter *)mj_footer{
    if (mj_footer != self.mj_footer) {
        // 删除旧的,添加新的
        [self.mj_footer removeFromSuperview];
        [self insertSubview:mj_footer atIndex:0];
        
        // 存储新的
        [self willChangeValueForKey:@"mj_footer"]; // KVO
        objc_setAssociatedObject(self, &MJRefreshFooterKey,
                                 mj_footer, OBJC_ASSOCIATION_ASSIGN);
        [self didChangeValueForKey:@"mj_footer"]; // KVO
    }
}
3.3状态改变

MJRefresher 整个框架都是通过设置不同的状态,进而显示不同的子控件和改变view的位置。这里主要跟踪footer的状态变化和各个状态下footer的位置处理。

3.3.0状态变化
  • 初始化

    - (instancetype)initWithFrame:(CGRect)frame{
        if (self = [super initWithFrame:frame]) {
            // 准备工作
            [self prepare];
            
            // 默认是普通状态
            self.state = MJRefreshStateIdle;
        }
        return self;
    }
    

    设置闲置状态。

  • beginRefreshing / endRefreshing

    - (void)beginRefreshing{
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
            self.alpha = 1.0;
        }];
        self.pullingPercent = 1.0;
        // 只要正在刷新,就完全显示
        if (self.window) {
            self.state = MJRefreshStateRefreshing;
        } else {
            // 预防正在刷新中时,调用本方法使得header inset回置失败
            if (self.state != MJRefreshStateRefreshing) {
                self.state = MJRefreshStateWillRefresh;
                // 刷新(预防从另一个控制器回到这个控制器的情况,回来要重新刷新一下)
                [self setNeedsDisplay];
            }
        }
    }
    //**********************************divider******************************************//
    - (void)endRefreshing{
        dispatch_async(dispatch_get_main_queue(), ^{
            self.state = MJRefreshStateIdle;
        });
    }
    
  • scrollViewContentOffsetDidChange

    #pragma mark - 实现父类的方法
    
    - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{
        [super scrollViewContentOffsetDidChange:change];
        
        // 如果正在刷新,直接返回
        if (self.state == MJRefreshStateRefreshing) return;
        
        _scrollViewOriginalInset = self.scrollView.mj_inset;
        
        // 当前的contentOffset
        CGFloat currentOffsetY = self.scrollView.mj_offsetY;
        // 尾部控件刚好出现的offsetY
        CGFloat happenOffsetY = [self happenOffsetY];
        // 如果是向下滚动到看不见尾部控件,直接返回
        if (currentOffsetY <= happenOffsetY) return;
        
        CGFloat pullingPercent = (currentOffsetY - happenOffsetY) / self.mj_h;
        
        // 如果已全部加载,仅设置pullingPercent,然后返回
        if (self.state == MJRefreshStateNoMoreData) {
            self.pullingPercent = pullingPercent;
            return;
        }
        
        if (self.scrollView.isDragging) {
            self.pullingPercent = pullingPercent;
            // 普通 和 即将刷新 的临界点
            CGFloat normal2pullingOffsetY = happenOffsetY + self.mj_h;
            
            if (self.state == MJRefreshStateIdle && currentOffsetY > normal2pullingOffsetY){
                // 转为即将刷新状态
                self.state = MJRefreshStatePulling;
            } else if (self.state == MJRefreshStatePulling && currentOffsetY <= normal2pullingOffsetY) {
                // 转为普通状态
                self.state = MJRefreshStateIdle;
            }
        } else if (self.state == MJRefreshStatePulling) {// 即将刷新 && 手松开
            // 开始刷新
            [self beginRefreshing];
        } else if (pullingPercent < 1) {
            self.pullingPercent = pullingPercent;
        }
    }
    
  • 其他

    - (void)endRefreshingWithNoMoreData{
        dispatch_async(dispatch_get_main_queue(), ^{
            self.state = MJRefreshStateNoMoreData;
        });
    }
    
    - (void)noticeNoMoreData{
        [self endRefreshingWithNoMoreData];
    }
    
    - (void)resetNoMoreData{
        dispatch_async(dispatch_get_main_queue(), ^{
            self.state = MJRefreshStateIdle;
        });
    }
    
3.3.1MJRefreshStateRefreshing
- (void)setState:(MJRefreshState)state{
    MJRefreshCheckState
    
    // 根据状态来设置属性
    if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
        // 刷新完毕
        // ......
    } else if (state == MJRefreshStateRefreshing) {
        // 记录刷新前的数量
        self.lastRefreshCount = self.scrollView.mj_totalDataCount;
        
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
            CGFloat bottom = self.mj_h + self.scrollViewOriginalInset.bottom;
            CGFloat deltaH = [self heightForContentBreakView];
            if (deltaH < 0) { // 如果内容高度小于view的高度
                bottom -= deltaH;
            }
            self.lastBottomDelta = bottom - self.scrollView.mj_insetB;
            self.scrollView.mj_insetB = bottom;
            self.scrollView.mj_offsetY = [self happenOffsetY] + self.mj_h;
        } completion:^(BOOL finished) {
            [self executeRefreshingCallback];
        }];
    }
}

刷新状态时,设置insetBottom:当内容高度大于View的高度,bottom为刷新控件的高+最初的inset.bottom;当内容高度小于View的高度时,bottom为刷新控件的高+最初的inset.bottom+内容没填满View的高度。

内容高度大于View的高度

内容高度小于View的高度

只有当设置 inset.bottom 占领的空间足够大,一直于显示区域无法完全显示content,往上拉动以致显示footer。(有点不知道怎么解释清楚)

可参考

3.3.2pulling
- (void)setPullingPercent:(CGFloat)pullingPercent{
    
}

在上拉拖动的过程,根据拖动的百分比来设置UI。

3.3.3闲置状态/MJRefreshStateNoMoreData
if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
        // 刷新完毕
        if (MJRefreshStateRefreshing == oldState) {
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                self.scrollView.mj_insetB -= self.lastBottomDelta;
                
                // 自动调整透明度
                if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
            } completion:^(BOOL finished) {
                self.pullingPercent = 0.0;
                
                if (self.endRefreshingCompletionBlock) {
                    self.endRefreshingCompletionBlock();
                }
            }];
        }
        
        CGFloat deltaH = [self heightForContentBreakView];
        // 刚刷新完毕
        if (MJRefreshStateRefreshing == oldState && deltaH > 0 && self.scrollView.mj_totalDataCount != self.lastRefreshCount) {
            self.scrollView.mj_offsetY = self.scrollView.mj_offsetY;
        }
    }

结束刷新的时候,将UIScrollView的contentInset设置回原来的位置。

4.总结

MJRefresher 框架非常容易扩展,如何设计一款容易扩展的框架值得我们去学习。MJRefresher 是通过父类来设置状态,子类重写父类的状态办法来实现UI的设置和更新。

参考

MJRefresh

J_Knight_'s MJRefresh源码解析

从MJRefresh源码学习上拉下刷新的基本原理