iOS MJRefresh源码解析(MJRefreshComponent)

181 阅读5分钟

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

MJRefresh 下载地址:github.com/CoderMJLee/… 关于 MJRefresh 的使用就不多做介绍,主要是想讲一下他的源码。 首先是 MJRefresh 的一个框架图:

image.png 从框架图中可以看到 MJRefreshComponent 是所有控件的基类,在从基类中分成header跟footer,而后再进行细分的。所以首先要分析的就是 MJRefreshComponent类

1、MJRefreshComponent 解析

MJRefreshComponent.h

MJRefreshComponent.h 中的属性与方法的解释都写的很详细了,这里我就介绍一下所引用的一些类

#import <UIKit/UIKit.h>
#import "MJRefreshConst.h"  
#import "UIView+MJExtension.h"
#import "UIScrollView+MJExtension.h"
#import "UIScrollView+MJRefresh.h"
#import "NSBundle+MJRefresh.h"
类名功能
MJRefreshConst.hMJ 的一部分固定常量
UIView+MJExtension.h改变、获取视图的宽、高、尺寸等
UIScrollView+MJExtension.h改变、获取滚动时图的 insert 跟 offset 的值
UIScrollView+MJRefresh.h获取与添加新的 footer 跟 header
NSBundle+MJRefresh.h语言本地化跟箭头图标

这几个扩展里面的要点、难点不多,主要就是看如何使用category添加对象属性与KVO的手动刷新 其中有一个地方我比较感兴趣:

UIScrollView+MJRefresh.h 中的 mj_reloadData 方法

@implementation UITableView (MJRefresh)

+ (void)load
{
    [self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
}

- (void)mj_reloadData
{
    [self mj_reloadData];
    
    [self executeReloadDataBlock];
}
@end

exchangeInstanceMethod1: method2: 是将 Method1 与 Method2 的方法进行交换 在这里面就是在调用 UITableView 的 reloadData 方法的时候变成调用此处的 mj_reloadData 而在方法中的

[self mj_reloadData];

反而调用的是 UITableView 的 reloadData 方法,相当于为 reloadData 方法加了方法, 而如果直接如下代码则会产生死循环

- (void)reloadData 
{
    [self reloadData];

    [self executeReloadDataBlock];
}

UIScrollView+MJRefresh 中还有 mj_totalDataCount 与 (^mj_reloadDataBlock)(NSInteger totalDataCount) 属性 其主要作用就是在这是了 isAutomaticallyHidden 为 True 的情况下 mj_totalDataCount 为 0 的时候,将自动隐藏 footer,本人是基本没用到该方法,所以带过就好。

MJRefreshComponent.m

首先我们看一下 MJRefreshComponent.m 中的方法

image.png

image.png

初始化处理

首先在初始化的时候,要将属性设置为初始的状态

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

这里主要是重置状态到普通、背景透明色与宽度按比例缩放。 layoutSubViews 主要是为了子类调用 placeSubViews,即在 addsubview 的时候要调用 placeSubViews;

- (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;
        
        // 添加监听
        [self addObservers];
    }
}

_scrollViewOriginalInset 属性是为了在 Header 加载完毕或者在 BackFooter 加载完毕的时候可以返回到原来的位置; alwaysBounceVertical 属性是必备的,在设置弹簧效果关闭的时候,你会发现你无法使用 MJRefresh。

drawRect 可以按它字面意思理解,没什么好多讲的。

KVO监听

#pragma mark - KVO监听
- (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];
}

- (void)removeObservers
{
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentOffset];
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentSize];;
    [self.pan removeObserver:self forKeyPath:MJRefreshKeyPathPanState];
    self.pan = nil;
}

- (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{}

这一部分监听了三个属性

  1. scrollView 的 contentOffset;
  2. scrollView 的 contentSize;
  3. scrollView 的 panGestureRecognizer 的 状态。

所有的更改处理都交予子类, Component 当中只负责监听与调用方法。

接下来设置回调对象和回调方法只是属性赋值,而设置状态之后再调用 layoutSubViews 对子控件进行布局。

刷新状态处理

#pragma mark 进入刷新状态
- (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];
        }
    }
}

- (void)beginRefreshingWithCompletionBlock:(void (^)())completionBlock
{
    self.beginRefreshingCompletionBlock = completionBlock;
    
    [self beginRefreshing];
}

#pragma mark 结束刷新状态
- (void)endRefreshing
{
    self.state = MJRefreshStateIdle;
}

- (void)endRefreshingWithCompletionBlock:(void (^)())completionBlock
{
    self.endRefreshingCompletionBlock = completionBlock;
    
    [self endRefreshing];
}

#pragma mark 是否正在刷新
- (BOOL)isRefreshing
{
    return self.state == MJRefreshStateRefreshing || self.state == MJRefreshStateWillRefresh;
}

这三块内容都是刷新相关的、外界调用的,主要的功能就是设置开始刷新回调,结束刷新回调; 在开始刷新的方法中,判断如果已经放置在window上,则开始刷新,否则将状态置为准备刷新,并刷新界面(刷新就如同注释所说,如果是pop或者dismiss会到改控制器,则此时不在window中也不会再次调用,所以需要手动刷新);

透明度设置

#pragma mark 自动切换透明度
- (void)setAutoChangeAlpha:(BOOL)autoChangeAlpha
{
    self.automaticallyChangeAlpha = autoChangeAlpha;
}

- (BOOL)isAutoChangeAlpha
{
    return self.isAutomaticallyChangeAlpha;
}

- (void)setAutomaticallyChangeAlpha:(BOOL)automaticallyChangeAlpha
{
    _automaticallyChangeAlpha = automaticallyChangeAlpha;
    
    if (self.isRefreshing) return;
    
    if (automaticallyChangeAlpha) {
        self.alpha = self.pullingPercent;
    } else {
        self.alpha = 1.0;
    }
}

#pragma mark 根据拖拽进度设置透明度
- (void)setPullingPercent:(CGFloat)pullingPercent
{
    _pullingPercent = pullingPercent;
    
    if (self.isRefreshing) return;
    
    if (self.isAutomaticallyChangeAlpha) {
        self.alpha = pullingPercent;
    }
}

这部分就很简单了,就是赋值、设置透明度,其中拉拽的百分比是通过子类获取的,在 .h 文件中有说明。

调用刷新回调

#pragma mark - 内部方法
- (void)executeRefreshingCallback
{
    dispatch_async(dispatch_get_main_queue(), ^{
        if (self.refreshingBlock) {
            self.refreshingBlock();
        }
        if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
            MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
        }
        if (self.beginRefreshingCompletionBlock) {
            self.beginRefreshingCompletionBlock();
        }
    });
}

子类在结束刷新动画之后,调用刷新结束的回调,MJRefreshMsgSend 是 objc_msgSend 方法的格式化。是为了动态发送请求调用

// 运行时objc_msgSend
#define MJRefreshMsgSend(...) ((void (*)(void *, SEL, UIView *))objc_msgSend)(__VA_ARGS__)
#define MJRefreshMsgTarget(target) (__bridge void *)(target)

对这部分不了解的可以去查一下 Runtime。

今天就先写到这里~~,后面再写 header 与 footer~~。