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