当前位置: 移动技术网 > IT编程>移动开发>IOS > iOS: MJRefresh源码分析

iOS: MJRefresh源码分析

2018年12月26日  | 移动技术网IT编程  | 我要评论

网站收录入口,运城黑社会,中国人民抗日战争网上纪念馆

mjrefresh代码的核心思想

mjrefresh.png
上图为mjrefresh项目的项目结构

在mjrefresh中,使用了kvo、runtime、继承、gcd等知识

核心思想

mjrefreshcomponent是刷新控件的基类,在mjrefreshcomponent添加了kvo监听、prepare方法和placesubviews方法。

当mjrefreshcomponent中kvo监听到之后,响应会在mjrefreshheader和mjrefreshfooter中实现,mjrefreshheader和mjrefreshfooter其实响应kvo方法,主要就是设置state状态,然后在他们的子类中会分别调用setstate方法,根据不同的state状态进行不同的变化

prepare方法和placesubviews方法。prepare是设置刷新控件,包括文字、gif图片、风格等等;placesubviews是调整刷新控件的子控件的位置。在mjrefreshcomponent的每一个子类中都会先调用父类对应方法,然后根据自身的特点进行不同实现

mjrefresh分析

上一篇分析了mjrefresh的框架结构和核心思想,现在选择最简单的一个分支来进行分析。

mjrefreshnormalheader -> mjrefreshstateheader -> mjrefreshheader -> mjrefreshcomponent

一般开始使用mjrefresh的时候,往往都是几行代码就调用了,例如:

mjrefreshnormalheader *header = [mjrefreshnormalheader headerwithrefreshingblock:^{
    [self reloaddata];
    [self.tableview.mj_header endrefreshing];
}];
self.tableview.mj_header = header;

现在来看看,上面的几行代码到底经历着怎样的实现?

上面的代码,是创建了一个mjrefreshnormalheader的对象,然后将它赋给了self.tableview.mj_header,mj_header是什么呢?然后找到uiscrollview+mjrefresh.h文件,可以看到这是一个分类

#import 
#import "mjrefreshconst.h"

@class mjrefreshheader, mjrefreshfooter;

@interface uiscrollview (mjrefresh)
/** 下拉刷新控件 */
@property (strong, nonatomic) mjrefreshheader *mj_header;
@property (strong, nonatomic) mjrefreshheader *header mjrefreshdeprecated("使用mj_header");
/** 上拉刷新控件 */
@property (strong, nonatomic) mjrefreshfooter *mj_footer;
@property (strong, nonatomic) mjrefreshfooter *footer mjrefreshdeprecated("使用mj_footer");

#pragma mark - other
- (nsinteger)mj_totaldatacount;
@property (copy, nonatomic) void (^mj_reloaddatablock)(nsinteger totaldatacount);
@end

作者利用runtime的技巧给这个分类添加了五个属性和一个方法,然后将封装好的刷新控件添加给uiscrollview

- (void)setmj_header:(mjrefreshheader *)mj_header
{
    if (mj_header != self.mj_header) {
    // 删除旧的,添加新的
    [self.mj_header removefromsuperview];
    [self insertsubview:mj_header atindex:0];

    // 存储新的
    [self willchangevalueforkey:@"mj_header"]; // kvo
    objc_setassociatedobject(self, &mjrefreshheaderkey,
                             mj_header, objc_association_assign);
    [self didchangevalueforkey:@"mj_header"]; // kvo
    }
}

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

所以其实现在可以理解,self.tableview.mj_header = header;其实就是给tableview添加一个头部的刷新控件.而增加的属性mjrefreshheader就是刚才创建的mjrefreshnormalheader的基类。mjrefreshheader继承于mjrefreshcomponent, mjrefreshcomponent是整个刷新控件的基类。

创建了mjrefreshnormalheader的对象,直接调用了一个类方法headerwithrefreshingblock,这个方法是它父类mjrefreshheader的一个方法

“mjrefreshheader.m”文件
+ (instancetype)headerwithrefreshingblock:(mjrefreshcomponentrefreshingblock)refreshingblock
{
    mjrefreshheader *cmp = [[self alloc] init];
    cmp.refreshingblock = refreshingblock;
    return cmp;
}

此方法是为了创建一个mjrefreshheader对象,在创建对象init的时候,又会调用到mjrefreshheader的父类mjrefreshcomponent的方法

@implementation mjrefreshcomponent
#pragma mark - 初始化
- (instancetype)initwithframe:(cgrect)frame
{
    if (self = [super initwithframe:frame]) {
        // 准备工作
        [self prepare];

        // 默认是普通状态
        self.state = mjrefreshstateidle;
    }
    return self;
}

注意:mjrefreshcomponent 类中的prepare方法,会被它的子类都进行调用,每个字类的prepare方法,都会调用父类中的prepare方法,然后增加自己特有的执行操作。
执行完init方法,最后会返回一个mjrefreshnormalheader对象,然后添加给self.scrollview,添加上去后,便会开始执行mjrefreshcomponent中的- (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 = 0;

        // 记录uiscrollview
        _scrollview = (uiscrollview *)newsuperview;
        // 设置永远支持垂直弹簧效果
        _scrollview.alwaysbouncevertical = yes;
        // 记录uiscrollview最开始的contentinset
        _scrollvieworiginalinset = _scrollview.contentinset;

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

监听了三个值,分别是uiscrollview的contentoffset、contentsize、滑动手势的状态

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

利用kvo监听到之后,都会响应相应的didchange方法,比如下拉刷新,下拉必然会让contentoffset发生变化,必然会响应对应的方法:

mjrefreshheader文件
- (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;
    }
}

上面的其实就是根据拖动的时候,scrollview的contentoffset的变化进行state的设置:临界点就是scrollview的inset.top与刷新控件的高度相加的值。进行相应的操作,然后更改state,在每一次更改state的时候,就发生了哪些变化呢,看看下面的方法

mjrefreshheader文件
- (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];
            }];
        });
    }
}

执行setstate方法的时候,进行了界面的操作。如果是正常状态的时候,恢复inset和offset;如果是刷新状态,那就设置inset和offset,将scrollview的视图往下挤一点。

再看看mjrefreshnormalheader文件的实现

mjrefreshnormalheader文件
#pragma mark - 重写父类的方法
- (void)prepare
{
    [super prepare];

    self.activityindicatorviewstyle = uiactivityindicatorviewstylegray;
}

- (void)placesubviews
{
    [super placesubviews];

    // 箭头的中心点
    cgfloat arrowcenterx = self.mj_w * 0.5;
    if (!self.statelabel.hidden) {
        cgfloat statewidth = self.statelabel.mj_textwith;
        cgfloat timewidth = 0.0;
        if (!self.lastupdatedtimelabel.hidden) {
            timewidth = self.lastupdatedtimelabel.mj_textwith;
        }
        cgfloat textwidth = max(statewidth, timewidth);
        arrowcenterx -= textwidth / 2 + self.labelleftinset;
    }
    cgfloat arrowcentery = self.mj_h * 0.5;
    cgpoint arrowcenter = cgpointmake(arrowcenterx, arrowcentery);

    // 箭头
    if (self.arrowview.constraints.count == 0) {
        self.arrowview.mj_size = self.arrowview.image.size;
        self.arrowview.center = arrowcenter;
    }

    // 圈圈
    if (self.loadingview.constraints.count == 0) {
        self.loadingview.center = arrowcenter;
    }

    self.arrowview.tintcolor = self.statelabel.textcolor;
}

- (void)setstate:(mjrefreshstate)state
{
    mjrefreshcheckstate

    // 根据状态做事情
    if (state == mjrefreshstateidle) {
        if (oldstate == mjrefreshstaterefreshing) {
            self.arrowview.transform = cgaffinetransformidentity;

            [uiview animatewithduration:mjrefreshslowanimationduration animations:^{
                self.loadingview.alpha = 0.0;
            } completion:^(bool finished) {
                // 如果执行完动画发现不是idle状态,就直接返回,进入其他状态
                if (self.state != mjrefreshstateidle) return;

                self.loadingview.alpha = 1.0;
                [self.loadingview stopanimating];
                self.arrowview.hidden = no;
            }];
        } else {
            [self.loadingview stopanimating];
            self.arrowview.hidden = no;
            [uiview animatewithduration:mjrefreshfastanimationduration animations:^{
                self.arrowview.transform = cgaffinetransformidentity;
         }];
        }
    } else if (state == mjrefreshstatepulling) {
        [self.loadingview stopanimating];
        self.arrowview.hidden = no;
        [uiview animatewithduration:mjrefreshfastanimationduration animations:^{
            self.arrowview.transform = cgaffinetransformmakerotation(0.000001 - m_pi);
        }];
    } else if (state == mjrefreshstaterefreshing) {
        self.loadingview.alpha = 1.0; // 防止refreshing -> idle的动画完毕动作没有被执行
        [self.loadingview startanimating];
        self.arrowview.hidden = yes;
    }
}

上面的placesubviews方法设置了刷新控件的子控件的位置以及大小,然后setstate方法就是更加具体的根据不同state来进行界面的变换:当state由刷新变为正常时,停止loadingview的动画,显示箭头;当state状态为pulling的时候,箭头会发生变化,转个方向;当state为刷新时,loadingview开始动画,隐藏箭头。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网