开源库信息:

MJRefresh是目前用得比较多的下拉刷新,上拉加载开源库了,它支持UIScrollViewUITableViewUICollectionViewUIWebView

整个类结构图如下:

下面是在网上找的比较好的一张图,也附带给大家

在开始讲解MJRefresh源码之前大家最好对UIScrollView的各个尺寸数据有个明确的认识,下面是在网上找的一个比较好的图大家可以对照着这个来看:

MJRefreshComponent

MJRefreshComponent是所有上拉下拉控件的基类,它主要是通过KVO实现对scrollView contentOffsetcontentSize,以及scrollView 手势状态也就是: self.scrollView.panGestureRecognizer state的监听,来分别触发下面的对应方法,这些方法在不同的子类都有自己的实现,这个后面会具体展开介绍:

// scrollView ContentOffset改变的时候触发
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
// scrollView ContentSize改变的时候触发
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
// scrollView 手势状态改变的时候触发
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}

MJRefreshHeader

MJRefreshHeader主要负责通过ContentOffset的变化以及是否正在滑动状态来确定上拉,下滑控件的状态。并负责上一次刷新时间的记录。

MJRefreshStateHeader

MJRefreshStateHeader 负责状态文本以及上一次更新时间文本的显示。

MJRefreshNormalHeader

MJRefreshNormalHeader 负责箭头和加载菊花的状态显示。

MJRefreshGifHeader

MJRefreshGifHeader 负责加载Gif图片的状态控制

MJRefreshFooter

MJRefreshFooter 主要负责上拉控件状态的维护

MJRefreshAutoFooter

MJRefreshAutoFooter 负责维护ContentOffset 与 state的对应关系。

MJRefreshAutoStateFooter

MJRefreshAutoStateFooter 主要负责状态文本的控制。

MJRefreshAutoNormalFooter

MJRefreshAutoNormalFooter 主要负责加载菊花的状态控制

MJRefreshAutoGifFooter

MJRefreshAutoGifFooter 主要负责Gif类型的加载动画控制

MJRefreshBackStateFooter

MJRefreshBackStateFooter 负责自动返回类型上拉控件的状态文本控制。

MJRefreshBackNormalFooter

MJRefreshBackNormalFooter负责自动返回类型上拉控件的加载菊花状态控制。

MJRefreshBackGifFooter

MJRefreshBackGifFooter 负责自动返回类型上拉控件加载Gif动画的播放控制。

MJRefreshConst

MJRefreshConst 里面放置的是整个MJRefresh的常量以及关键宏定义。

UIScrollView+MJExtension

UIScrollView+MJExtension存放的是UIScrollView尺寸位置数据的便捷方法

UIView+MJExtension
UIView+MJExtension 和 UIScrollView+MJExtension类似存放的是UIView尺寸位置数据的便捷方法

UIScrollView+MJRefresh

UIScrollView+MJRefresh 中通过关联属性方式为UIScrollView添加了MJRefreshHeader和MJRefreshFooter

NSBundle+MJRefresh

NSBundle+MJRefresh 是 MJRefresh 内的文本以及图片资源获取方法。

MJRefreshConfig
用于存放MJRefresh的配置的类,目前只存放languageCode,暂时没多大用处。

其实整个大的方向还是遵循

手动滑动ScrollView --> ScrollView ContentOffset / 滑动状态 发生变化 --> MJRefresh 状态发生变化 --> 触发对应的改变,比如上一次更新时间,状态文本,菊花状态等等。

下面就顺着这个思路来对MJRefresh源码进行解析:

源码解析

MJRefreshComponent

状态常量的定义

/** 刷新控件的状态 */
typedef NS_ENUM(NSInteger, MJRefreshState) {
/** 普通闲置状态 */
MJRefreshStateIdle = 1,
/** 松开就可以进行刷新的状态 */
MJRefreshStatePulling,
/** 正在刷新中的状态 */
MJRefreshStateRefreshing,
/** 即将刷新的状态 */
MJRefreshStateWillRefresh,
/** 所有数据加载完毕,没有更多的数据了 */
MJRefreshStateNoMoreData
};

状态改变的回调类型定义

/** 进入刷新状态的回调 */
typedef void (^MJRefreshComponentRefreshingBlock)(void);
/** 开始刷新后的回调(进入刷新状态后的回调) */
typedef void (^MJRefreshComponentBeginRefreshingCompletionBlock)(void);
/** 结束刷新后的回调 */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)(void);

关键属性

/** 刷新状态 一般交给子类内部实现 */
@property (assign, nonatomic) MJRefreshState state;
/** 记录scrollView刚开始的inset */
@property (assign, nonatomic, readonly) UIEdgeInsets scrollViewOriginalInset;
/** 父控件 */
@property (weak, nonatomic, readonly) UIScrollView *scrollView;
/** 拉拽的百分比(交给子类重写) */
@property (assign, nonatomic) CGFloat pullingPercent;
/** 根据拖拽比例自动切换透明度 */
@property (assign, nonatomic, getter=isAutomaticallyChangeAlpha) BOOL automaticallyChangeAlpha;
/** 是否正在刷新 */
@property (assign, nonatomic, readonly, getter=isRefreshing) BOOL refreshing;

状态触发,及状态关键节点回调

状态触发:

/** 进入刷新状态 */
- (void)beginRefreshing;
- (void)beginRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 结束刷新状态 */
- (void)endRefreshing;
- (void)endRefreshingWithCompletionBlock:(void (^)(void))completionBlock;

状态关键节点回调:

/** 正在刷新的回调 */
@property (copy, nonatomic, nullable) MJRefreshComponentRefreshingBlock refreshingBlock;
/** 开始刷新后的回调(进入刷新状态后的回调) */
@property (copy, nonatomic, nullable) MJRefreshComponentBeginRefreshingCompletionBlock beginRefreshingCompletionBlock;
/** 带动画的结束刷新的回调 */
@property (copy, nonatomic, nullable) MJRefreshComponentEndRefreshingCompletionBlock endRefreshingAnimateCompletionBlock;
/** 结束刷新的回调 */
@property (copy, nonatomic, nullable) MJRefreshComponentEndRefreshingCompletionBlock endRefreshingCompletionBlock;

供给子类实现的方法

/** 初始化 */
- (void)prepare NS_REQUIRES_SUPER;
/** 摆放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 当scrollView的contentOffset发生改变的时候调用 */
- (void)scrollViewContentOffsetDidChange:(nullable NSDictionary *)change NS_REQUIRES_SUPER;
/** 当scrollView的contentSize发生改变的时候调用 */
- (void)scrollViewContentSizeDidChange:(nullable NSDictionary *)change NS_REQUIRES_SUPER;
/** 当scrollView的拖拽状态发生改变的时候调用 */
- (void)scrollViewPanStateDidChange:(nullable NSDictionary *)change NS_REQUIRES_SUPER;

我们截取官方Demo例子作为一个场景来对整个代码解析:

__unsafe_unretained UITableView *tableView = self.tableView;
tableView.mj_header= [MJRefreshNormalHeader headerWithRefreshingBlock:^{
//......
}];

当然我们会先将注意力聚焦在MJRefreshComponent。

1. 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];
}

这部分的工作主要是初始状态的设置。在设置状态的时候会调用state的setter。这里面会触发布局,这也是为什么autoresizingMask一定要在prepare进行设置的原因:

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

对于MJRefreshComponent这里面,placeSubviews是空的。主要是对子类placeSubviews的触发。

- (void)layoutSubviews {
[self placeSubviews];
[super layoutSubviews];
}

所以初始化过程主要完成如下任务:

  • 调用prepare做一些准备工作
  • 设置MJRefresh的状态为Idle状态
  • 触发布局

2. MJRefreshComponent添加到父控件:

新建MJRefreshComponent子类成功后会通过setMj_header添加到UIScrollView或者它的子类上。

- (void)setMj_header:(MJRefreshHeader *)mj_header {
if (mj_header != self.mj_header) {
// 删除旧的,添加新的
[self.mj_header removeFromSuperview];
[self insertSubview:mj_header atIndex:0];
// 存储新的
objc_setAssociatedObject(self, &MJRefreshHeaderKey,
mj_header, OBJC_ASSOCIATION_RETAIN);
}
}

在调用insertSubview 的时候 MJRefreshComponent的 willMoveToSuperview:

- (void)willMoveToSuperview:(UIView *)newSuperview {
[super willMoveToSuperview:newSuperview];
// 如果不是UIScrollView,不做任何事情
if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
// 旧的父控件移除监听
[self removeObservers];
if (newSuperview) { // 新的父控件
// 记录UIScrollView
_scrollView = (UIScrollView *)newSuperview;
// 设置宽度
self.mj_w = _scrollView.mj_w;
// 设置位置
self.mj_x = -_scrollView.mj_insetL;
// 设置永远支持垂直弹簧效果
_scrollView.alwaysBounceVertical = YES;
// 记录UIScrollView最开始的contentInset
_scrollViewOriginalInset = _scrollView.mj_inset;

// 添加监听
[self addObservers];
}
}

willMoveToSuperview 中会先移除对旧父控件的关键事件监听,监听新的父UIScollView的关键事件,并记录下UIScrollView最开始的contentInset。

addObservers 方法中主要通过KVO方式让MJRefreshComponent监听父控件scrollView的ContentOffset,ContentSize 以及
scrollView的手势状态。

- (void)addObservers {
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];
[self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];
self.pan = self.scrollView.panGestureRecognizer;
[self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];
}

一旦监听的变量发生变化那么就会到observeValueForKeyPath中进行处理:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// 遇到这些情况就直接返回
if (!self.userInteractionEnabled) return;

// 这个就算看不见也需要处理
if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
[self scrollViewContentSizeDidChange:change];
}

// 看不见
if (self.hidden) return;
if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
[self scrollViewContentOffsetDidChange:change];
} else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
[self scrollViewPanStateDidChange:change];
}
}

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}

````

其中ContentOffset 以及scrollView的手势状态 只有在可见的时候才会触发子类对应的方法。

****3. 状态的管理:****

MJRefreshComponent 中的setState主要是提供给子类设置的,但是在MJRefreshComponent中的beginRefreshing以及endRefreshing也会触发MJRefresh的状态,beginRefreshing会将状态设置为MJRefreshStateRefreshing,endRefreshing中会将状态设置为MJRefreshStateIdle。

  • (void)beginRefreshing {
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
    self.alpha = 1.0;
    }];
    self.pullingPercent = 1.0;
    // 只要正在刷新,就完全显示
    if (self.window) {
    self.state = MJRefreshStateRefreshing;
    } else {
    //…..
    }
    }

  • (void)endRefreshing {
    MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateIdle;)
    }


    ****4. 下拉进度的管理:****

    和setState一样下拉进度也是受contentOffset影响的,所以下拉进度的设置也是提供给子类调用的。下拉进度的改变会导致下拉控件透明度的同步变化。

  • (void)setPullingPercent:(CGFloat)pullingPercent {
    _pullingPercent = pullingPercent;
    if (self.isRefreshing) return;
    if (self.isAutomaticallyChangeAlpha) {
    self.alpha = pullingPercent;
    }
    }


    ****MJRefreshStateHeader****

    MJRefreshComponent像是一个没有灵魂的父类,它只负责监听scrollView的contentOffset,contentSize,以及触摸状态的变化,将这些变化传递给子类。

  • (void)scrollViewContentOffsetDidChange:(NSDictionary *)change {

    [super scrollViewContentOffsetDidChange:change];
    // 在刷新的refreshing状态
    if (self.state == MJRefreshStateRefreshing) {
    [self resetInset];
    return;
    }
    // 跳转到下一个控制器时,contentInset可能会变, 这时候将最初的contentInset保存在_scrollViewOriginalInset
    _scrollViewOriginalInset = self.scrollView.mj_inset;

    // 当前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 头部控件刚好出现的offsetY,这里由于UIScrollView默认是贴着可见的边缘,也就是导航栏的底部。所以可以用- self.scrollViewOriginalInset.top来表示。
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;

    // 如果是向上滚动到看不见头部控件,直接返回
    // >= -> >
    if (offsetY > happenOffsetY) return;

    // 普通 和 即将刷新 的临界点,normal2pullingOffsetY 表示MJRefreshHeader完全露出来的位置
    CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
    // 这个是MJRefreshHeader露出来的百分比
    CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;

    if (self.scrollView.isDragging) { // 如果正在拖拽
    self.pullingPercent = pullingPercent;
    // 如果当前是空闲状态,并且偏移量超过了完全露出来的距离,但是由于当前正在处于拖拽状态则状态还是MJRefreshStatePulling
    if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
    // 转为即将刷新状态
    self.state = MJRefreshStatePulling;
    //如果当前处于MJRefreshStatePulling状态,偏移量小于完全露出来的距离,那么状态改为MJRefreshStateIdle
    } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
    // 转为普通状态
    self.state = MJRefreshStateIdle;
    }
    //如果松收那么就切换到MJRefreshStateRefreshing状态
    } else if (self.state == MJRefreshStatePulling) {// 即将刷新 && 手松开
    // 开始刷新
    [self beginRefreshing];
    } else if (pullingPercent < 1) {
    self.pullingPercent = pullingPercent;
    }

}


scrollViewContentOffsetDidChange 主要负责设置MJRefreshHeader的state。影响到state的有两方面原因:
  1. 是否处于拖拽状态
  2. 当前UIScrollView的offset与下拉头完全露出来的位置的关系

    如果当前是空闲状态,并且偏移量超过了下拉头部完全露出来的距离,但是由于当前正在处于拖拽状态则状态还是MJRefreshStatePulling,在处于拖拽状态下,如果当前处于MJRefreshStatePulling状态,偏移量小于完全露出来的距离,那么状态改为MJRefreshStateIdle。如果松手那么就切换到MJRefreshStateRefreshing状态。并触发beginRefreshing调用上层业务进行刷新。

    下面我们来看下状态的设置:

  • (void)setState:(MJRefreshState)state {
    MJRefreshCheckState
    // 根据状态做事情
    if (state == MJRefreshStateIdle) {
    if (oldState != MJRefreshStateRefreshing) return;

    // 保存刷新时间
    [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
    [[NSUserDefaults standardUserDefaults] synchronize];

    // 恢复inset和offset
    [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
    self.scrollView.mj_insetT += self.insetTDelta;
    if (self.endRefreshingAnimateCompletionBlock) {
    self.endRefreshingAnimateCompletionBlock();
    }
    // 自动调整透明度
    if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
    } completion:^(BOOL finished) {
    self.pullingPercent = 0.0;
    if (self.endRefreshingCompletionBlock) {
    self.endRefreshingCompletionBlock();
    }
    }];
    } else if (state == MJRefreshStateRefreshing) {
    MJRefreshDispatchAsyncOnMainQueue({
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
    if (self.scrollView.panGestureRecognizer.state != UIGestureRecognizerStateCancelled) {
    CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
    // 增加滚动区域top
    self.scrollView.mj_insetT = top;
    // 设置滚动位置
    CGPoint offset = self.scrollView.contentOffset;
    offset.y = -top;
    [self.scrollView setContentOffset:offset animated:NO];
    }
    } completion:^(BOOL finished) {
    [self executeRefreshingCallback];
    }];
    })
    }

}


如果上一次为MJRefreshStateRefreshing,当前状态为MJRefreshStateIdle,表示一次刷新结束。这时候会将当前时间记录为上一次刷新时间记录在本地。并且以动画的形式将header隐藏,并在动画结束以及完全隐藏的时候调用****endRefreshingAnimateCompletionBlock********endRefreshingCompletionBlock****,如果状态要设置为MJRefreshStateRefreshing,那么需要通过setContentOffset将头部显示出来。并在完成后调用executeRefreshingCallback。

也就是说在进入refreshing状态的时候会调用refreshingBlock以及beginRefreshingCompletionBlock,在回到idle状态的时候会调用endRefreshingAnimateCompletionBlock以及endRefreshingCompletionBlock。

****MJRefreshAutoFooter****

我们看到MJRefreshFooter有两类MJRefreshAutoFooter以及MJRefreshBackFooter,MJRefreshAutoFooter的特点是在内容超过一个屏幕的时候,不松手的情况下只要footer露出来了就会自动启动刷新,这会持续autoTriggerTimes次,当然我们可以设置autoTriggerTimes为负数来支持无限次自动加载。在松手的情况下则会在指定的条件下触发刷新。
MJRefreshBackFooter的footer只会出现在UIScrollView的底部,并且只有在松手的情况下才会启动刷新。

我们先来看下MJRefreshAutoFooter,在新建MJRefreshAutoFooter的时候会先调用prepare方法:

  • (void)prepare {
    [super prepare];
    // 默认底部控件100%出现时才会自动刷新
    self.triggerAutomaticallyRefreshPercent = 1.0;
    // 设置为默认状态
    self.automaticallyRefresh = YES;
    // 自动刷新的次数
    self.autoTriggerTimes = 1;
    }


    在prepare方法中设置了触发自动刷新的百分比,以及将触发状态设置为自动刷新状态,autoTriggerTimes被设置为1次。autoTriggerTimes是上拉不松手的情况下触发自动刷新的次数超过这个次数,上拉只有松手的情况下才会触发刷新,如果要自动刷新次数不受限那么将autoTriggerTimes设置为-1即可。

    创建好后在我们将MJRefreshAutoFooter添加到UIScrollView上面的时候会调用willMoveToSuperview,这时候ContentInset Bottom会相应地增加mj_h,如果newSuperview = nil 表示从父控件中移除,ContentInset Bottom会减少mj_h。

  • (void)willMoveToSuperview:(UIView *)newSuperview {
    [super willMoveToSuperview:newSuperview];
    if (newSuperview) { // 新的父控件
    if (self.hidden == NO) {
    self.scrollView.mj_insetB += self.mj_h;
    }
    // 设置位置
    self.mj_y = _scrollView.mj_contentH;
    } else { // 被移除了
    if (self.hidden == NO) {
    self.scrollView.mj_insetB -= self.mj_h;
    }
    }
    }


    mj_h是在MJRefreshFooter prepare方法中设置的。

    一旦UIScrollView完成数据的加载就会触发ContentSize发生变化,scrollViewContentSizeDidChange会被调用,这时候MJRefreshAutoFooter的位置就会重新被设置,它会被追加到UIScrollView 内容的最底部,注意不是UIScrollView的最底部。

  • (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
    [super scrollViewContentSizeDidChange:change];
    // 设置位置
    self.mj_y = self.scrollView.mj_contentH + self.ignoredScrollViewContentInsetBottom;
    }


    在内容超过一个屏幕后,如果我们向上拉页面,拉到footer显示出triggerAutomaticallyRefreshPercent百分比后即使我们不松手的情况下也会自动触发beginRefreshing进行刷新。

  • (void)scrollViewContentOffsetDidChange:(NSDictionary *)change {
    [super scrollViewContentOffsetDidChange:change];

    if (self.state != MJRefreshStateIdle || !self.automaticallyRefresh || self.mj_y == 0) return;

    if (_scrollView.mj_insetT + _scrollView.mj_contentH > _scrollView.mj_h) { // 内容超过一个屏幕
    // 这里的_scrollView.mj_contentH替换掉self.mj_y更为合理
    if (_scrollView.mj_offsetY >= _scrollView.mj_contentH - _scrollView.mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + _scrollView.mj_insetB - self.mj_h) {
    // 防止手松开时连续调用
    CGPoint old = [change[@”old”] CGPointValue];
    CGPoint new = [change[@”new”] CGPointValue];
    if (new.y <= old.y) return;

    if (_scrollView.isDragging) {
    self.triggerByDrag = YES;
    }
    // 当底部刷新控件完全出现时,才刷新
    [self beginRefreshing];
    }
    }

}


如果我们手松开的时候,如果不够一个屏幕,那么在拉到offsetY超过顶部insetTop的时候开始刷新,如果超出一个屏幕,也会在拉到见到footer的时候开始刷新。

  • (void)scrollViewPanStateDidChange:(NSDictionary *)change {
    [super scrollViewPanStateDidChange:change];
    if (self.state != MJRefreshStateIdle) return;
    UIGestureRecognizerState panState = _scrollView.panGestureRecognizer.state;
    switch (panState) {
    // 手松开
    case UIGestureRecognizerStateEnded: {
    if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h) { // 不够一个屏幕
    if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { // 向上拽
    self.triggerByDrag = YES;
    [self beginRefreshing];
    }
    } else { // 超出一个屏幕
    if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView.mj_insetB - _scrollView.mj_h) {
    self.triggerByDrag = YES;
    [self beginRefreshing];
    }
    }
    } break;
    case UIGestureRecognizerStateBegan: {
    [self resetTriggerTimes];
    } break;
    default: break;
    }
    }


    状态设置:

  • (void)setState:(MJRefreshState)state {
    MJRefreshCheckState

    if (state == MJRefreshStateRefreshing) {
    [self executeRefreshingCallback];
    } else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
    if (self.triggerByDrag) {
    if (!self.unlimitedTrigger) {
    self.leftTriggerTimes -= 1;
    }
    self.triggerByDrag = NO;
    }
    if (MJRefreshStateRefreshing == oldState) {
    if (self.scrollView.pagingEnabled) {
    CGPoint offset = self.scrollView.contentOffset;
    offset.y -= self.scrollView.mj_insetB;
    [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
    self.scrollView.contentOffset = offset;
    if (self.endRefreshingAnimateCompletionBlock) {
    self.endRefreshingAnimateCompletionBlock();
    }
    } completion:^(BOOL finished) {
    if (self.endRefreshingCompletionBlock) {
    self.endRefreshingCompletionBlock();
    }
    }];
    return;
    }
    if (self.endRefreshingCompletionBlock) {
    self.endRefreshingCompletionBlock();
    }
    }
    }

}


****MJRefreshBackFooter****

MJRefreshBackFooter 和 MJRefreshAutoFooter不同的是它会永远出现在UIScrollView的最底部,为啥?我们看下代码:

  • (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);
    }


    在内容高度大于UIScrollView的时候,它会追加在内容最后面,这时候MJRefreshBackFooter也是在UIScrollView的外面,在滑到UIScrollView的时候开始加载。如果UIScrollView的高度大于内容高度,那么它会被加在UIScrollView最底部。

    我们接下来看下MJRefreshBackFooter的状态变化:

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

}


这里有两个比较关键的offset:

****happenOffsetY****: 它是MJRefreshBackFooter刚好露出来时候UIScrollView的ContentOffset.
****normal2pullingOffsetY****: 它是MJRefreshBackFooter完全露出来时候UIScrollView的ContentOffset.

所以 currentOffsetY但凡大于happenOffsetY就表示MJRefreshBackFooter已经露出来了,这时候,如果scrollView处于拖拽状态,并且currentOffsetY > normal2pullingOffsetY 表示整个MJRefreshBackFooter已经完全露出来了,这时候会切换到MJRefreshStatePulling状态,如果currentOffsetY <= normal2pullingOffsetY那么当前状态为MJRefreshStateIdle。如果处于MJRefreshStatePulling状态,也就是MJRefreshBackFooter已经完全露出来的情况下,松手的话就会触发刷新。

下面是状态改变的过程:

  • (void)setState:(MJRefreshState)state {
    MJRefreshCheckState
    // 根据状态来设置属性
    if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
    // 刷新完毕
    if (MJRefreshStateRefreshing == oldState) {
    [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
    self.scrollView.mj_insetB -= self.lastBottomDelta;
    if (self.endRefreshingAnimateCompletionBlock) {
    self.endRefreshingAnimateCompletionBlock();
    }
    // 自动调整透明度
    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;
    }
    } 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];
    }];
    }
    }
    ```

MJRefresh最核心的部分代码已经介绍完毕了,后续的一些,比如包含State的Header以及Footer 都是用于处理与状态文本相关的类,包含Normal的Header以及Footer都是用于处理箭头和菊花状态的类,而包含Gif的Header以及Footer都是用于处理Gif动画的类,这些类都是基于state, pullingPercent 这些关键属性的变化而改变的,所以理解上面介绍的如何根据Content Offset 变化 确定state以及pullingPercent是理解MJRefresh的核心。

Contents
  1. 1. 开源库信息:
  2. 2. 源码解析