41

MJRefresh源码解读

 5 years ago
source link: http://www.cocoachina.com/ios/20181207/25745.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

MJRefresh是MJ大神写的一个实现上拉刷新和下拉刷新的第三方库,这个库目前在很多有名的应用上都有使用看,下面就来分析一下MJRefresh的源码。

1.简单应用

下面创建一个绿色的UIScrollview,然后在UIScrollview上加上一个红色的视图作为子视图:

self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 20, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
    self.scrollView.backgroundColor = [UIColor greenColor];
    [self.view addSubview:_scrollView];

    self.scrollView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(refresh)];

    self.scrollView.contentInset = UIEdgeInsetsMake(54, 0, 0, 0);

    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.scrollView.frame.size.width, self.scrollView.frame.size.height)];
    view.backgroundColor = [UIColor redColor];
    [self.scrollView addSubview:view];

然后我们看一下效果:

qmIvYrN.gif

在这里我们设置的contentInset.top = 54,54正是这个下拉控件的高度,所以整个下拉控件是完全可见的。

2.源码分析

我们首先看一下MJRefresh的源码的类的结构,由于上拉刷新和下拉刷新控件的原理基本一致,因此这里我们仅使用下拉刷新控件来分析:

QjQNjea.png!web下面我们从下拉刷新控件的使用开始来探索源码:

self.scrollView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(refresh)];
首先我们的疑问是UIScrollview的mj_header这个属性是哪里来的?我们找到 UIScrollview+MJRefresh.h 这个文件,这是写的UIScrollview的一个分类,在这个分类中我们找到了mj_header属性,mj_footer属性,但是分类申明属性是没有set方法和get方法的,那么怎么去赋值和取值呢?这时候就要用到runtime的关联属性方法了,我们在 UIScrollview+MJRefresh.m 文件中找到 - (void)setMj_header:(MJRefreshHeader *)mj_header 方法看看是不是像我猜想的那样:

rieMZbF.png!web

事实证明确实是这样,使用关联属性来设值和取值。
MJRefreshNormalHeader 最终是继承自 MJRefreshComponent ,那么我们就先从 MJRefreshComponent 来看:
首先在MJRefresh.h文件中有一个枚举类表示刷新控件的状态:

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

其中默认状态是 MJRefreshStateIdle , 当我们拖拽UIScrollview的时候在没有到达临界点之前都是这个状态,当我们拖拽到了临界点之后就变成了 MJRefreshStatePulling 状态,这个状态就是松手就可以刷新的状态,当我们在 MJRefreshStatePulling 状态下松手就变成了 MJRefreshStateRefreshing 状态,即正在刷新状态。

MJRefreshComponnet 中有下列属性:

  • @property (weak, nonatomic) id refreshingTarget;
    这是回调的对象

  • @property (assign, nonatomic) SEL refreshingAction;
    这是回调的方法

  • @property (assign, nonatomic) MJRefreshState state;
    刷新控件的状态

  • @property (assign, nonatomic, readonly) UIEdgeInsets scrollViewOriginalInset;
    scrollview刚开始的contentInset

  • @property (assign, nonatomic) CGFloat pullingPercent;
    拉拽的百分比,用来控制刷新控件的透明度

MJRefreshComponent.m 中的核心方法是: - (void)willMoveToSuperview:(UIView *)newSuperview ,这个方法是在视图加入父视图或者改变父视图的时候调用:

- (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_w, _scrollViewOriginalInset 这三个属性并且添加了观察者:

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

监听了scrollview的contentOffset和contentsize的改变,需要根据contentoffset的改变来变更刷新控件的状态。

MJRefreshHeader

接下来再来分析一下 MJRefreshComponnet 的子类 MJRefreshHeader 这个类,这个类是基础的下拉刷新控件类:

这个类的头文件中多了两个方法,这两个方法都是用来创建下拉刷新控件的,不同的是一个的回调是block,另一个的回调是@selector:

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

在这个类中确定了上拉刷新控件的mj_y属性:

- (void)placeSubviews
{
    [super placeSubviews];

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

由于 self.ignoredScrollViewContentInsetTop 一般是0,所以一般情况下就有:

self.mj_y = -self.mj_h;

然后我们再来看一下一个最核心的方法: - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change ,这个方法是contentoffset发生变化时KVO产生的调用,

MZZvuiZ.png!web

上面这个方法主要是根据contentoffset进行了一系列判断然后进行了状态的变化,那么在这个类里面还有一个很重要的方法,就是 - (void)setState:(MJRefreshState)state ,我们看看这个方法里面做了什么:

MVJ7Jny.png!web

总结一下 MJRefreshHeader 这个类,这个类实现了两个非常重要的方法,一个是 - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change ,这个方法是在用手拖拽scrollview导致contentoffset变化的时候调用的,在这个方法中会根据contentoffset的值来改变下拉刷新控件的状态。这个类实现的另一个很重要的方法是 - (void)setState:(MJRefreshState)state ,在 - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change 中改变属性,在 - (void)setState:(MJRefreshState)state 中根据属性的改变来做具体的事。

MJRefreshStateHeader

** MJRefreshStateHeader 继承自 MJRefreshHeader ,这个类是带有状态文字的刷新控件,没有箭头和菊花。这个类比较简单,主要是对状态label和最近刷新时间label进行布局:

MJJfm2U.png!web

MJRefreshNoramlHeaderMJRefreshStateHeader 的子类,它是默认的下拉刷新控件类,我们实例中用的就是这种,这种刷新控件是在拖拽的时候显示箭头,当开始刷新的时候箭头小时,显示菊花。

MJRefreshNoramlHeader 这个类做了两件事,一件事是布局上面说到的箭头视图和菊花视图,另外一件事是处理在拖拽过程中箭头和菊花的变化。

布局菊花和箭头:

AnaQz23.png!web

处理拖拽过程中箭头和菊花的变化

VzIfUjU.png!web

MJRefreshGifHeader

MJRefreshGifHeader 这个类也是继承自 MJRefreshStateHeader ,它是带GIF的下拉刷新控件,就是把 MJRefreshNormalHeader 的箭头和菊花变成GIF,下面我们先看一下 MJRefreshNormal 的简单使用:

MJRefreshGifHeader *header = [MJRefreshGifHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewData)];
// Set the ordinary state of animated images
[header setImages:idleImages forState:MJRefreshStateIdle];
// Set the pulling state of animated images(Enter the status of refreshing as soon as loosen)
[header setImages:pullingImages forState:MJRefreshStatePulling];
// Set the refreshing state of animated images
[header setImages:refreshingImages forState:MJRefreshStateRefreshing];
// Set header
self.tableView.mj_header = header;

下面我们就从 - (void)setImages:(NSArray *)images forState:(MJRefreshState)state 这个方法下手来看看具体实现:

- (void)setImages:(NSArray *)images forState:(MJRefreshState)state 
{ 
    [self setImages:images duration:images.count * 0.1 forState:state]; 
}

这个方法还是主要调用 - (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state 这个方法:

- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state 
{ 
    if (images == nil) return; 

    self.stateImages[@(state)] = images; 
    self.stateDurations[@(state)] = @(duration); 

    /* 根据图片设置控件的高度 */ 
    UIImage *image = [images firstObject]; 
    if (image.size.height > self.mj_h) { 
        self.mj_h = image.size.height; 
    } 
}

在这个方法中,使用了两个字典,一个字典用来存放各种状态下的图片数组,因为GIF本质上也就是多张图片循环播放嘛,另一个字典用来存放各种状态下GIF动画的周期。并且如果GIF图片的高度大于刷新控件的高度,那么就调整刷新控件的高度为GIF图片的高度。

我们再来看一下GIF视图的布局:

niaAJfi.png!web

根据状态变化显示不同的GIF视图:

7NfQZzQ.png!web

这篇文章在简书的地址: MJRefresh源码解读

作者:雪山飞狐_91ae


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK