欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS: MJRefresh源码分析

程序员文章站 2022-07-06 12:02:29
mjrefresh代码的核心思想 上图为mjrefresh项目的项目结构 在mjrefresh中,使用了kvo、runtime、继承、gcd等知识 核心思想 – mjrefreshco...

mjrefresh代码的核心思想

iOS: MJRefresh源码分析
上图为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开始动画,隐藏箭头。