MJRefresh源码解析(MJRefreshHeader 和 MJRefreshFooter)

273 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情

上一篇文章的传送门: juejin.cn/post/715662…

MJRefreshHeader 与 MJRefreshFooter 都是继承于 MJRefreshComponent的,也是后续详细的 header、footer的基础父类。

关于头文件就不多做说明,已有足够的注释说明。


2、MJRefreshHeader 解析

image.png

首先可以看所有实现的方法:

  1. 两个构造方法
  2. 四个覆盖函数的方法
  3. 两个公共方法

构造方法(类方法)

#pragma mark - 构造方法
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
    MJRefreshHeader *cmp = [[self alloc] init];
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    MJRefreshHeader *cmp = [[self alloc] init];
    [cmp setRefreshingTarget:target refreshingAction:action];
    return cmp;
}

构造方法的功能就是创建一个对象,并把回调或者方法写入对象当中,以便刷新的时候可以调用。

初始化处理

/// 初始化
- (void)prepare
{
    [super prepare];
    
    // 设置key
    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
    
    // 设置高度
    self.mj_h = MJRefreshHeaderHeight;
}

lastUpdatedTimeKey 是把 MJRefreshHeaderLastUpdatedTimeKey 直接写入其中,作为存储上一次刷新时间的 key。(只不过不知道为什么不直接使用 MJRefreshHeaderLastUpdatedTimeKey 而用 lastUpdatedTimeKey 作为中间替代呢?) mj_h 此时初始化为值 54.0,留作后面处理。(固定值,一直不变)

/// 摆放子控件
- (void)placeSubviews
{
    [super placeSubviews];
    
    // 设置y值(当自己的高度发生改变了,肯定要重新调整Y值,所以放到placeSubviews方法中设置y值)
    self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}

这里已经有了注释了,就略过了。 ignoredScrollViewContentInsetTop 是 忽略多少scrollView的contentInset的top。

监听 scrollView 的滚动变化(核心功能)

/// 监听 scrollView 的 ContentOffset 发生变化之后的处理
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    // 在刷新的refreshing状态
    if (self.state == MJRefreshStateRefreshing) {
        if (self.window == nil) return;
        
        // sectionheader停留解决
        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;
    }
    
    // 跳转到下一个控制器时,contentInset可能会变
     _scrollViewOriginalInset = self.scrollView.contentInset;
    
    // 当前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 头部控件刚好出现的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 如果是向上滚动到看不见头部控件,直接返回
    // >= -> >
    if (offsetY > happenOffsetY) return;
    
    // 普通 和 即将刷新 的临界点
    CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
    CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
    
    if (self.scrollView.isDragging) { // 如果正在拖拽
        self.pullingPercent = pullingPercent;
        if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
            // 转为即将刷新状态
            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;
    }
}

处理 refresh 的视图保留放在顶部, insetT 是为了设置 refresh 视图与顶部位置的偏移(UIEdgeInsets.top)

CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top; 

这部分是判断现在的 scrollview 的 -y 是否大于 scrollview 的初始 insert 的顶部距离,并取其中最大的值。

insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;

这部分是判断自身的高加上 scrollview 的初始 insert 的顶部距离与刚才算出来的最大值的大小,并选取最小值。 insetTDelta 是用来保存 insetT 与顶部距离的间隔。

    // 跳转到下一个控制器时,contentInset可能会变
    _scrollViewOriginalInset = self.scrollView.contentInset;

由于每次跳转都有可能改变 contentInset 所以需要每次都保存,防止发生变化。

    // 当前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 头部控件刚好出现的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 如果是向上滚动到看不见头部控件,直接返回
    // >= -> >
    if (offsetY > happenOffsetY) return;
    
    // 普通 和 即将刷新 的临界点
    CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
    CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;

已有注解,略过。

if (self.scrollView.isDragging) { // 如果正在拖拽
        self.pullingPercent = pullingPercent;
        if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
            // 转为即将刷新状态
            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;
    }

此时一共分为三种状态:

  1. 正在拖拽时;
  2. 状态等于即将刷新 && 手松开;
  3. 手松开并且状态不等于即将刷新。

如果在拖拽状态: 则会在闲置状态与拖拽状态通过计算offset 是否超过普通或者即将刷新的临界点并进行切换。 如果在即将刷新 && 手松开: 则直接进行刷新。 如果手松开并且状态不等于即将刷新: 保存拖拽的百分比,用作后面处理。

状态的 setter 方法重写(核心功能)

/// 设置状态处理
- (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.isAutomaticallyChangeAlpha) self.alpha = 0.0;
        } completion:^(BOOL finished) {
            self.pullingPercent = 0.0;
            
            if (self.endRefreshingCompletionBlock) {
                self.endRefreshingCompletionBlock();
            }
        }];
    } else if (state == MJRefreshStateRefreshing) {
         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];
            }];
         });
    }
}

其中需要注意 MJRefreshCheckState 是一个宏:

#define MJRefreshCheckState \
MJRefreshState oldState = self.state; \
if (state == oldState) return; \
[super setState:state];
 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.isAutomaticallyChangeAlpha) self.alpha = 0.0;
        } completion:^(BOOL finished) {
            self.pullingPercent = 0.0;
            
            if (self.endRefreshingCompletionBlock) {
                self.endRefreshingCompletionBlock();
            }
        }];
    }

如果当前状态是闲置且先前状态为刷新的话,则执行以下操作:

  1. 保存刷新时间;
  2. 动画恢复inset.如果开启了根据拖拽比例自动切换透明度,则透明度动画置0;
  3. 当前拖拽百分比置0,且执行结束刷新block。

公共方法处理

#pragma mark - 公共方法
- (void)endRefreshing
{
    dispatch_async(dispatch_get_main_queue(), ^{
        self.state = MJRefreshStateIdle;
    });
}

- (NSDate *)lastUpdatedTime
{
    return [[NSUserDefaults standardUserDefaults] objectForKey:self.lastUpdatedTimeKey];
}

给子类调用的,分别是设置结束刷新时候的状态和获取上次刷新时间。

3、MJRefreshFooter 解析

image.png MJRefreshFooter 与 MJRefreshHeader 相比,只多了三个方法

/** 提示没有更多的数据 */
- (void)endRefreshingWithNoMoreData;
- (void)noticeNoMoreData MJRefreshDeprecated("使用endRefreshingWithNoMoreData");

/** 重置没有更多的数据(消除没有更多数据的状态) */
- (void)resetNoMoreData;

三个方法同属一种状态:MJRefreshStateNoMoreData 的不同变更。

公共方法

#pragma mark - 公共方法
- (void)endRefreshingWithNoMoreData
{
    self.state = MJRefreshStateNoMoreData;
}

- (void)noticeNoMoreData
{
    [self endRefreshingWithNoMoreData];
}

- (void)resetNoMoreData
{
    self.state = MJRefreshStateIdle;
}

在 footer 对于自身的实现处理很少,除了构造方法与初始化之外,只实现了监听scrollView数据的变化的回调。其余的都在子类中实现。

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    if (newSuperview) {
        // 监听scrollView数据的变化
        if ([self.scrollView isKindOfClass:[UITableView class]] || [self.scrollView isKindOfClass:[UICollectionView class]]) {
            [self.scrollView setMj_reloadDataBlock:^(NSInteger totalDataCount) {
                if (self.isAutomaticallyHidden) {
                    self.hidden = (totalDataCount == 0);
                }
            }];
        }
    }
}

Mj_reloadDataBlock 在 reloaddate 执行之后执行,totalDataCount 是 tableview 的 row 的总数,只有在开启 AutomaticallyHidden 的时候才会执行(手动开启)。 至此,MJRefreshHeader 和 MJRefreshFooter 的解析也已经完成了。