iOS 李明杰 MJRefresh源码解析
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 李明杰 MJRefresh源码解析相关的知识,希望对你有一定的参考价值。
参考技术A MJRefresh 是著名开发者及培训讲师李明杰老师的作品,到现在在github已经有10000多颗star,真真是极好用的ios下拉刷新(也支持上拉加载更)控件。本文主要素材来源有 正在学hybrid开发的iOS开发者 J_Knight 的文章和 github 上李明杰老师的用法介绍(主要是本文下部分)。该框架的结构设计得很清晰,使用一个基类MJRefreshComponent来做一些基本的设定,然后通过继承的方式,让MJRefreshHeader和MJRefreshFooter分别具备下拉刷新和上拉加载的功能。从继承机构来看可以分为三层,具体可以从下面的图里看出来:
其中一条继承链是这样的
开发者一般使用场景是这样的:
作者 在类目 UIScrollView+MJRefresh 中运用运行时关联属性函数 objc_setAssociatedObject 和 objc_getAssociatedObject 对 UIScrollView 视图控件添加了 mj_header 、 mj_footer 等属性.
MJRefreshNormalHeader 是最下层的子类, headerWithRefreshingTarget 和 headerWithRefreshingBlock 是最上层基类 MJRefreshComponent 暴露出的静态快捷构造方法。把对象的创建下放到子类,此处明显用到了工厂方法。编译时类型 mj_header is classof MJRefreshHeader ,运行时类型 MJRefreshNormalHeader 是其子类,此处是典型的多态特性的应用。
即面向接口编码,而非面向实现编码。作者从最上层基类 MJRefreshComponent 即暴露接口
具体实现:
具体实现,首先来看一下该控件的基类:MJRefreshComponent:
这个类作为该控件基类,涵盖了基类所具备的成份:状态,回调block等,大致分成下面这5种职能:
监听的声明:offset、size、state
对于监听的处理:
从上面等结构图可以看出,紧接着这个基类,下面分为<code>MJRefreshHeader</code>和<code>MJRefreshFooter</code>,这里顺着<code>MJRefreshHeader</code>这个分支向下展开:
<code>MJRefreshHeader</code>继承于<code>MJRefreshComponent</code>,它做了这几件事:
初始化有两种方法:动作-目标/block块函数 做回调
通过重写<code>prepare</code>方法来设置header的高度:
通过重写placeSubviews方法来重新调整y值:
这里提供一张图来体现三个状态的不同:
这个类是<code>MJRefreshHeader</code>类的子类,它做了两件事:
给一张图,让大家直观感受一下这两个控件:
这个类通过覆盖父类三个方法来实现上述两个实现:
在这里,将每一个状态对应的提示文字放入一个字典里面,<code>key</code>是状态的<code>NSNumber</code>形式
这里主要是对<code>lastUpdatedTimeLabel</code>和<code>stateLabel</code>进行布局。要注意<code>lastUpdatedTimeLabel</code>隐藏的情况。
在这里,根据传入的<code>state</code>不同,在<code>stateLabel</code>和<code>lastUpdatedTimeLabel</code>里切换相应的文字。
<code>MJRefreshNormalHeader </code>继承于 <code>MJRefreshStateHeader</code>,它主要做了两件事:
给一张图来直观感受一下这两个view:
同<code>MJRefreshStateHeader</code>一样,也是重写了父类的三个方法:
到此为止,我们已经从<code>MJRefreshComponent</code>到<code>MJRefreshNormalHeader</code>的实现过程看了一遍。可以看出,作者将<code>prepare</code>,<code>placeSubviews</code>以及 setState: 方法作为基类的方法,让下面的子类去一层一层实现。
而每一层的子类,根据自身的职责,分别按照自己的方式来实现这三个方法:
这样做的好处是,如果想要增加某种类型的 header ,只要在某一层上做文章即可。例如该框架里的 MJRefreshGifHeader ,它和 MJRefreshNormalHeader 属于同一级,都是继承于 MJRefreshStateHeader 。因为二者都具有相同形式的 stateLabel 和 lastUpdatedTimeLabel ,唯一不同的就是左侧的部分:
还是提供一张图来直观感受一下:
下面我们来看实现:
它提供了两个接口,是用来设置不同状态下使用的图片数组的:
和MJRefreshNormalHeader一样,它也重写了基类提供的三个方法来实现显示gif图片的职能。
iOS MJRefresh源码分析
简介
MJRefresh是iOS开发中运用的最多的上拉加载更多和下拉刷新控件,是著名开发者及培训讲师李明杰老师的作品。
一、继承关系
MJRefresh中主要的类并不多,整个MJRefresh主要类的关系如下图所示:
二、MJRefreshComponent 基类
MJRefreshComponent 为整个控件的基类,其本身是一个UIView。
主要提供了以下功能:
- 声明控件的所有状态。
- 声明控件的回调函数。
- 添加监听。
- 提供刷新,停止刷新接口。
- 提供子类需要实现的方法。
2.1 控件的所有状态
/** 刷新控件的状态 */
typedef NS_ENUM(NSInteger, MJRefreshState)
/** 普通闲置状态 */
MJRefreshStateIdle = 1,
/** 松开就可以进行刷新的状态 */
MJRefreshStatePulling,
/** 正在刷新中的状态 */
MJRefreshStateRefreshing,
/** 即将刷新的状态 */
MJRefreshStateWillRefresh,
/** 所有数据加载完毕,没有更多的数据了 */
MJRefreshStateNoMoreData
;
2.2 控件的回调函数
/** 进入刷新状态的回调 */
typedef void (^MJRefreshComponentRefreshingBlock)();
/** 开始刷新后的回调(进入刷新状态后的回调) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)();
/** 结束刷新后的回调 */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)();
2.3 添加监听
主要是对 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];
对于监听的处理:
- (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];
#pragma mark- 以下监听响应方法都在本层不做实现,而是交给不同的子类分别对相同的方法做不同的实现,充分利用了面向对象语言的多态特性。
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change
- (void)scrollViewPanStateDidChange:(NSDictionary *)change
2.4 提供刷新,停止刷新接口
#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];
2.5 提供子类需要实现的方法
#pragma mark - 交给子类们去实现
/** 初始化 */
- (void)prepare NS_REQUIRES_SUPER;
/** 摆放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 当scrollView的contentOffset发生改变的时候调用 */
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 当scrollView的contentSize发生改变的时候调用 */
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 当scrollView的拖拽状态发生改变的时候调用 */
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
三、基础的下拉刷新 MJRefreshHeader
MJRefreshHeader 继承于 MJRefreshComponent。
这个类主要提供了以下功能:
初始化。
设置header高度。
重新调整y值。
根据 contentOffset 的变化,来切换状态。
3.1 初始化
初始化有两种方法:动作-目标/block块函数 做回调
#pragma mark - 构造方法
+ (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;
3.2 设置header高度
通过重写父类的 prepare 方法来设置 header 的高度:
#pragma mark - 覆盖父类的方法
- (void)prepare
[super prepare];
// 设置用于在NSUserDefaults里存储时间的key
self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
// 设置header的高度
self.mj_h = MJRefreshHeaderHeight;
3.3 重新调整y值
通过重写父类的 placeSubviews 方法来重新调整y值:
#pragma mark - 覆盖父类的方法
- (void)placeSubviews
[super placeSubviews];
// 设置y值(当自己的高度发生改变了,肯定要重新调整Y值,所以放到placeSubviews方法中设置y值)
self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
3.4 根据 contentOffset 的变化切换状态
#pragma mark - 覆盖父类的方法
- (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;
这里的状态有三种:默认状态(MJRefreshStateIdle),可以刷新的状态(MJRefreshStatePulling)以及正在刷新的状态(MJRefreshStateRefreshing)。
状态切换的因素有两个:一个是 下拉的距离是否超过临界值,另一个是手指是否离开屏幕。
可以刷新的状态和正在刷新的状态是不同的。因为在手指还贴在屏幕的时候是不能进行刷新的。所以即使在下拉的距离超过了临界距离(状态栏 + 导航栏 + header高度),如果手指没有离开屏幕,那么也不能马上进行刷新,而是将状态切换为:可以刷新。一旦手指离开了屏幕,马上将状态切换为正在刷新。
3.5 状态切换时的相应操作
- (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];
];
);
这里状态的切换,主要围绕着两种:默认状态和正在刷新状态。也就是针对开始刷新和结束刷新这两个切换点。
从正在刷新状态状态切换为默认状态时( 结束刷新 ),需要记录刷新结束的时间。因为header里面有一个默认的label是用来显示上次刷新的时间的。
四、带有状态文字的下拉刷新 MJRefreshStateHeader
MJRefreshStateHeader 继承于基础的下拉刷新 MJRefreshHeader。
这个类主要提供了以下功能:
- 简单布局了stateLabel和lastUpdatedTimeLabel。
- 根据控件状态的切换(默认状态,正在刷新状态),实现了这两个label显示的文字的切换。
这个类通过覆盖父类三个方法来实现上述两个功能。
4.1 prepare 方法
在这里,将每一个状态对应的提示文字放入一个字典里面,key是状态的 NSNumber 形式。
#pragma mark - 覆盖父类的方法
- (void)prepare
[super prepare];
// 初始化间距
self.labelLeftInset = MJRefreshLabelLeftInset;
// 初始化文字
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing];
#pragma mark - 公共方法
- (void)setTitle:(NSString *)title forState:(MJRefreshState)state
if (title == nil) return;
self.stateTitles[@(state)] = title;
self.stateLabel.text = self.stateTitles[@(self.state)];
4.2 placeSubviews 方法
这里主要是对lastUpdatedTimeLabel
和stateLabel
进行布局。要注意lastUpdatedTimeLabel
隐藏的情况。
#pragma mark - 覆盖父类的方法
- (void)placeSubviews
[super placeSubviews];
if (self.stateLabel.hidden) return;
BOOL noConstrainsOnStatusLabel = self.stateLabel.constraints.count == 0;
if (self.lastUpdatedTimeLabel.hidden)
// 状态
if (noConstrainsOnStatusLabel) self.stateLabel.frame = self.bounds;
else
CGFloat stateLabelH = self.mj_h * 0.5;
// 状态
if (noConstrainsOnStatusLabel)
self.stateLabel.mj_x = 0;
self.stateLabel.mj_y = 0;
self.stateLabel.mj_w = self.mj_w;
self.stateLabel.mj_h = stateLabelH;
// 更新时间
if (self.lastUpdatedTimeLabel.constraints.count == 0)
self.lastUpdatedTimeLabel.mj_x = 0;
self.lastUpdatedTimeLabel.mj_y = stateLabelH;
self.lastUpdatedTimeLabel.mj_w = self.mj_w;
self.lastUpdatedTimeLabel.mj_h = self.mj_h - self.lastUpdatedTimeLabel.mj_y;
4.3 setState 方法
- (void)setState:(MJRefreshState)state
MJRefreshCheckState
// 设置状态文字
self.stateLabel.text = self.stateTitles[@(state)];
// 重新设置key(重新显示时间)
self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
五、默认的下拉刷新 MJRefreshNormalHeader
MJRefreshNormalHeader 继承于带有状态文字的下拉刷新 MJRefreshStateHeader。
这个类主要提供了以下功能:
- 添加了 arrowView 和 loadingView。
- 状态切换的时候改变 arrowView 和 loadingView 的样式。
同 MJRefreshStateHeader 一样,也是重写了父类的三个方法:
5.1 prepare 方法
#pragma mark - 重写父类的方法
- (void)prepare
[super prepare];
self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
5.2 placeSubviews 方法
- (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;
5.3 setState 方法
- (void)setState:(MJRefreshState)state
MJRefreshCheckState
// 根据状态更新arrowView和loadingView的显示
if (state == MJRefreshStateIdle)
// 1. 设置为默认状态
if (oldState == MJRefreshStateRefreshing)
// 1.1 从正在刷新状态中切换过来
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
// 1.2 从其他状态中切换过来
[self.loadingView stopAnimating];
// 显示箭头并设置为初始状态
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^
self.arrowView.transform = CGAffineTransformIdentity;
];
else if (state == MJRefreshStatePulling)
// 2. 设置为可以刷新状态
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^
// 箭头倒立
self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
];
else if (state == MJRefreshStateRefreshing)
// 3. 设置为正在刷新状态
self.loadingView.alpha = 1.0; // 防止refreshing -> idle的动画完毕动作没有被执行
// 菊花旋转
[self.loadingView startAnimating];
// 隐藏arrowView
self.arrowView.hidden = YES;
六、带动图的下拉刷新 MJRefreshGifHeader
MJRefreshGifHeader 继承于带有状态文字的下拉刷新 MJRefreshStateHeader。
这个类主要提供了以下功能:
- 添加了下拉刷新loading的动画样式。
同 MJRefreshNormalHeader 一样,也是重写了父类的三个方法:
6.1 prepare 方法
- (void)prepare
[super prepare];
// 初始化间距
self.labelLeftInset = 20;
6.2 placeSubviews 方法
根据label的宽度和存在与否设置gif的位置。
- (void)placeSubviews
[super placeSubviews];
//如果约束存在,就立即返回
if (self.gifView.constraints.count) return;
self.gifView.frame = self.bounds;
if (self.stateLabel.hidden && self.lastUpdatedTimeLabel.hidden)
//如果stateLabel和lastUpdatedTimeLabel都在隐藏状态,将gif剧中显示
self.gifView.contentMode = UIViewContentModeCenter;
else
//如果stateLabel和lastUpdatedTimeLabel中至少一个存在,则根据label的宽度设置gif的位置
self.gifView.contentMode = UIViewContentModeRight;
CGFloat stateWidth = self.stateLabel.mj_textWith;
CGFloat timeWidth = 0.0;
if (!self.lastUpdatedTimeLabel.hidden)
timeWidth = self.lastUpdatedTimeLabel.mj_textWith;
CGFloat textWidth = MAX(stateWidth, timeWidth);
self.gifView.mj_w = self.mj_w * 0.5 - textWidth * 0.5 - self.labelLeftInset;
6.3 setState 方法
根据传入状态的不同来设置动画。
- (void)setState:(MJRefreshState)state
MJRefreshCheckState
if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing)
//1. 如果传进来的状态是可以刷新和正在刷新
NSArray *images = self.stateImages[@(state)];
if (images.count == 0) return;
[self.gifView stopAnimating];
if (images.count == 1)
//1.1 单张图片
self.gifView.image = [images lastObject];
else
//1.2 多张图片
self.gifView.animationImages = images;
self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
[self.gifView startAnimating];
else if (state == MJRefreshStateIdle)
//2.如果传进来的状态是默认状态
[self.gifView stopAnimating];
小结
到此为止,我们已经从 MJRefreshComponent 到 MJRefreshGifHeader的实现过程看了一遍。可以看出,作者将 prepare,placeSubviews 以及 setState 方法作为基类的方法,让下面的子类去一层一层实现。而每一层的子类,根据自身的职责,分别按照自己的方式来实现这三个方法。
MJRefreshHeader:负责 header 的高度和调整 header 自身在外部的位置。
MJRefreshStateHeader:负责 header内部的 stateLabel 和 lastUpdatedTimeLabel 的布局和不同状态下内部文字的显示。
MJRefreshNormalHeader:负责 header 内部的 loadingView 以及 arrowView 的布局和不同状态下的显示。
MJRefreshGifHeader:负责 header 内部的 loadingView 以及 arrowView 的布局和不同状态下的显示。
这样做的好处是,如果想要增加某种类型的 header ,只要在某一层上做文章即可。例如该框架里的MJRefreshGifHeader 和 MJRefreshNormalHeader 属于同一级,都是继承于 MJRefreshStateHeader。因为二者都具有相同形式的 stateLabel 和 lastUpdatedTimeLabel,唯一不同的就是左侧的部分,MJRefreshNormalHeader 的左侧是箭头,MJRefreshGifHeader的左侧则是一个gif动画。
七、基础的上拉加载 MJRefreshFooter
MJRefreshFooter 继承于 MJRefreshComponent。
这个类主要提供了以下功能:
初始化。
设置header高度。
提供公共方法。
7.1 初始化
初始化有两种方法:动作-目标/block块函数 做回调
#pragma mark - 构造方法
+ (instancetype)footerWithRefreshingBlock:(MJRefreshComponentAction)refreshingBlock
MJRefreshFooter *cmp = [[self alloc] init];
cmp.refreshingBlock = refreshingBlock;
return cmp;
+ (instancetype)footerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
MJRefreshFooter *cmp = [[self alloc] init];
[cmp setRefreshingTarget:target refreshingAction:action];
return cmp;
7.2 设置header高度
通过重写父类的 prepare 方法来设置 header 的高度:
#pragma mark - 重写父类的方法
- (void)prepare
[super prepare];
// 设置自己的高度
self.mj_h = MJRefreshFooterHeight;
// 默认不会自动隐藏
// self.automaticallyHidden = NO;
7.3 提供公共方法
#pragma mark - 公共方法
- (void)endRefreshingWithNoMoreData
MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateNoMoreData;)
- (void)noticeNoMoreData
[self endRefreshingWithNoMoreData];
- (void)resetNoMoreData
MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateIdle;)
七、会回弹到底部的上拉加载 MJRefreshBackFooter
MJRefreshFooter 继承于 MJRefreshComponent。
这个类主要提供了以下功能:
- 重新调整y值。
- 切换状态,对应的操作。
7.1 重新调整y值
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change
[super scrollViewContentSizeDidChange:change];
// 内容的高度
CGFloat contentHeight = self.scrollView.mj_contentH + self.ignoredScrollViewContentInsetBottom;
// 表格的高度
CGFloat scrollHeight = self.scrollView.mj_h - self.scrollViewOriginalInset.top - self.scrollViewOriginalInset.bottom + self.ignoredScrollViewContentInsetBottom;
// 设置位置和尺寸
self.mj_y = MAX(contentHeight, scrollHeight);
7.2 重新调整y值
- (void)setState:(MJRefreshState)state
MJRefreshCheckState
// 根据状态来设置属性
if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle)
// 刷新完毕
if (MJRefreshStateRefreshing == oldState)
[UIView animateWithDuration:self.slowAnimationDuration animations:^
if (self.endRefreshingAnimationBeginAction)
self.endRefreshingAnimationBeginAction();
self.scrollView.mj_insetB -= self.lastBottomDelta;
// 自动调整透明度
if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
completion:^(BOOL finished)
self.pullingPercent = 0.0;
if (self.endRefreshingCompletionBlock)
self.endRefreshingCompletionBlock();
];
CGFloat deltaH = [self heightForContentBreakView];
// 刚刷新完毕
if (MJRefreshStateRefreshing == oldState && deltaH > 0 && self.scrollView.mj_totalDataCount != self.lastRefreshCount)
self.scrollView.mj_offsetY = self.scrollView.mj_offsetY;
else if (state == MJRefreshStateRefreshing)
// 记录刷新前的数量
self.lastRefreshCount = self.scrollView.mj_totalDataCount;
[UIView animateWithDuration:self.fastAnimationDuration animations:^
CGFloat bottom = self.mj_h + self.scrollViewOriginalInset.bottom;
CGFloat deltaH = [self heightForContentBreakView];
if (deltaH < 0) // 如果内容高度小于view的高度
bottom -= deltaH;
self.lastBottomDelta = bottom - self.scrollView.mj_insetB;
self.scrollView.mj_insetB = bottom;
self.scrollView.mj_offsetY = [self happenOffsetY] + self.mj_h;
completion:^(BOOL finished)
[self executeRefreshingCallback];
];
八、带有状态文字的上拉加载 MJRefreshBackStateFooter
MJRefreshBackStateFooter 继承于 MJRefreshBackFooter。
这个类主要提供了以下功能:
- 显示刷新的状态。
8.1 prepare 方法
重写父类的方法
#pragma mark - 重写父类的方法
- (void)prepare
[super prepare];
// 初始化间距
self.labelLeftInset = MJRefreshLabelLeftInset;
[self textConfiguration];
- (void)textConfiguration
// 初始化文字
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshBackFooterIdleText] forState:MJRefreshStateIdle];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshBackFooterPullingText] forState:MJRefreshStatePulling];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshBackFooterRefreshingText] forState:MJRefreshStateRefreshing];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshBackFooterNoMoreDataText] forState:MJRefreshStateNoMoreData];
8.2 placeSubviews 方法
- (void)placeSubviews
[super placeSubviews];
if (self.stateLabel.constraints.count) return;
// 状态标签
self.stateLabel.frame = self.bounds;
8.3 setState 方法
- (void)setState:(MJRefreshState)state
MJRefreshCheckState
// 设置状态文字
self.stateLabel.text = self.stateTitles[@(state)];
九、默认的上拉加载 MJRefreshBackNormalFooter
MJRefreshBackNormalFooter 继承于 MJRefreshBackStateFooter。
这个类主要提供了以下功能:
- 添加了 arrowView 和 loadingView。
- 状态切换的时候改变 arrowView 和 loadingView 的样式。
重写了父类的三个方法:
9.1 prepare 方法
#pragma mark - 重写父类的方法
- (void)prepare
[super prepare];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *))
_activityIndicatorViewStyle = UIActivityIndicatorViewStyleMedium;
return;
#endif
_activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
9.2 placeSubviews 方法
- (void)placeSubviews
[super placeSubviews];
// 箭头的中心点
CGFloat arrowCenterX = self.mj_w * 0.5;
if (!self.stateLabel.hidden)
arrowCenterX -= self.labelLeftInset + self.stateLabel.mj_textWidth * 0.5;
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;
9.3 setState
- (void)setState:(MJRefreshState)state
MJRefreshCheckState
// 根据状态做事情
if (state == MJRefreshStateIdle)
if (oldState == MJRefreshStateRefreshing)
self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
[UIView animateWithDuration:self.slowAnimationDuration animations:^
self.loadingView.alpha = 0.0;
completion:^(BOOL finished)
// 防止动画结束后,状态已经不是MJRefreshStateIdle
if (self.state != MJRefreshStateIdle) return;
self.loadingView.alpha = 1.0;
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
];
else
self.arrowView.hidden = NO;
[self.loadingView stopAnimating];
[UIView animateWithDuration:self.fastAnimationDuration animations:^
self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
];
else if (state == MJRefreshStatePulling)
self.arrowView.hidden = NO;
[self.loadingView stopAnimating];
[UIView animateWithDuration:self.fastAnimationDuration animations:^
self.arrowView.transform = CGAffineTransformIdentity;
];
else if (state == MJRefreshStateRefreshing)
self.arrowView.hidden = YES;
[self.loadingView startAnimating];
else if (state == MJRefreshStateNoMoreData)
self.arrowView.hidden = YES;
[self.loadingView stopAnimating];
十、带动图的上拉加载 MJRefreshBackGifFooter
MJRefreshBackGifFooter 继承于 MJRefreshBackStateFooter。
这个类主要提供了以下功能:
- 添加了上拉刷新loading的动画样式。
重写了父类的三个方法:
10.1 prepare 方法
#pragma mark - 实现父类的方法
- (void)prepare
[super prepare];
// 初始化间距
self.labelLeftInset = 20;
10.2 placeSubviews 方法
- (void)placeSubviews
[super placeSubviews];
if (self.gifView.constraints.count) return;
self.gifView.frame = self.bounds;
if (self.stateLabel.hidden)
self.gifView.contentMode = UIViewContentModeCenter;
else
self.gifView.contentMode = UIViewContentModeRight;
self.gifView.mj_w = self.mj_w * 0.5 - self.labelLeftInset - self.stateLabel.mj_textWidth * 0.5;
10.3 setState 方法
- (void)setState:(MJRefreshState)state
MJRefreshCheckState
// 根据状态做事情
if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing)
NSArray *images = self.stateImages[@(state)];
if (images.count == 0) return;
self.gifView.hidden = NO;
[self.gifView stopAnimating];
if (images.count == 1) // 单张图片
self.gifView.image = [images lastObject];
else // 多张图片
self.gifView.animationImages = images;
self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
[self.gifView startAnimating];
else if (state == MJRefreshStateIdle)
self.gifView.hidden = NO;
else if (state == MJRefreshStateNoMoreData)
self.gifView.hidden = YES;
十一、会自动刷新的上拉加载 MJRefreshAutoFooter
MJRefreshAutoFooter 继承于 MJRefreshFooter。
这个类主要提供了以下功能:
- 添加自动刷新(无感刷新)的功能。
11.1 prepare 方法
#pragma mark - 实现父类的方法
- (void)prepare
[super prepare];
// 默认底部控件100%出现时才会自动刷新
self.triggerAutomaticallyRefreshPercent = 1.0;
// 设置为默认状态
self.automaticallyRefresh = YES;
self.autoTriggerTimes = 1;
11.2 setState 方法
- (void)setState:(MJRefreshState)state
MJRefreshCheckState
if (state == MJRefreshStateRefreshing)
[self executeRefreshingCallback];
else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle)
if (self.triggerByDrag)
if (!self.unlimitedTrigger)
self.leftTriggerTimes -= 1;
self.triggerByDrag = NO;
if (MJRefreshStateRefreshing == oldState)
if (self.scrollView.pagingEnabled)
CGPoint offset = self.scrollView.contentOffset;
offset.y -= self.scrollView.mj_insetB;
[UIView animateWithDuration:self.slowAnimationDuration animations:^
self.scrollView.contentOffset = offset;
if (self.endRefreshingAnimationBeginAction)
self.endRefreshingAnimationBeginAction();
completion:^(BOOL finished)
if (self.endRefreshingCompletionBlock)
self.endRefreshingCompletionBlock();
];
return;
if (self.endRefreshingCompletionBlock)
self.endRefreshingCompletionBlock();
11.3 scrollViewPanStateDidChange
- (void)scrollViewPanStateDidChange:(NSDictionary *)change
[super scrollViewPanStateDidChange:change];
if (self.state != MJRefreshStateIdle) return;
UIGestureRecognizerState panState = _scrollView.panGestureRecognizer.state;
switch (panState)
// 手松开
case UIGestureRecognizerStateEnded:
if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h) // 不够一个屏幕
if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) // 向上拽
self.triggerByDrag = YES;
[self beginRefreshing];
else // 超出一个屏幕
if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView.mj_insetB - _scrollView.mj_h)
self.triggerByDrag = YES;
[self beginRefreshing];
break;
case UIGestureRecognizerStateBegan:
[self resetTriggerTimes];
break;
default:
break;
十二、带状态文字的上拉加载 MJRefreshAutoStateFooter
MJRefreshAutoStateFooter 继承于 MJRefreshAutoFooter。
这个类主要提供了以下功能:
- 显示刷新的状态。
12.1 prepare 方法
#pragma mark - 重写父类的方法
- (void)prepare
[super prepare];
// 初始化间距
self.labelLeftInset = MJRefreshLabelLeftInset;
[self textConfiguration];
// 监听label
self.stateLabel.userInteractionEnabled = YES;
[self.stateLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(stateLabelClick)]];
12.2 placeSubviews 方法
- (void)placeSubviews
[super placeSubviews];
if (self.stateLabel.constraints.count) return;
// 状态标签
self.stateLabel.frame = self.bounds;
12.3 setState 方法
- (void)setState:(MJRefreshState)state
MJRefreshCheckState
if (self.isRefreshingTitleHidden && state == MJRefreshStateRefreshing)
self.stateLabel.text = nil;
else
self.stateLabel.text = self.stateTitles[@(state)];
十三、默认的上拉加载 MJRefreshAutoNormalFooter
MJRefreshAutoNormalFooter 继承于 MJRefreshAutoStateFooter。
这个类主要提供了以下功能:
- 添加了 loadingView。
- 状态切换的时候改变 loadingView 的样式。
13.1 prepare 方法
#pragma mark - 重写父类的方法
- (void)prepare
[super prepare];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *))
_activityIndicatorViewStyle = UIActivityIndicatorViewStyleMedium;
return;
#endif
_activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
13.2 placeSubviews 方法
- (void)placeSubviews
[super placeSubviews];
if (self.loadingView.constraints.count) return;
// 圈圈
CGFloat loadingCenterX = self.mj_w * 0.5;
if (!self.isRefreshingTitleHidden)
loadingCenterX -= self.stateLabel.mj_textWidth * 0.5 + self.labelLeftInset;
CGFloat loadingCenterY = self.mj_h * 0.5;
self.loadingView.center = CGPointMake(loadingCenterX, loadingCenterY);
13.3 setState 方法
- (void)setState:(MJRefreshState)state
MJRefreshCheckState
// 根据状态做事情
if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle)
[self.loadingView stopAnimating];
else if (state == MJRefreshStateRefreshing)
[self.loadingView startAnimating];
十四、带动画的上拉加载 MJRefreshAutoGifFooter
MJRefreshAutoGifFooter 继承于 MJRefreshAutoStateFooter。
这个类主要提供了以下功能:
- 添加上拉 loading 的动画效果。
14.1 prepare 方法
#pragma mark - 实现父类的方法
- (void)prepare
[super prepare];
// 初始化间距
self.labelLeftInset = 20;
14.2 placeSubviews 方法
- (void)placeSubviews
[super placeSubviews];
if (self.gifView.constraints.count) return;
self.gifView.frame = self.bounds;
if (self.isRefreshingTitleHidden)
self.gifView.contentMode = UIViewContentModeCenter;
else
self.gifView.contentMode = UIViewContentModeRight;
self.gifView.mj_w = self.mj_w * 0.5 - self.labelLeftInset - self.stateLabel.mj_textWidth * 0.5;
14.3 setState 方法
- (void)setState:(MJRefreshState)state
MJRefreshCheckState
// 根据状态做事情
if (state == MJRefreshStateRefreshing)
NSArray *images = self.stateImages[@(state)];
if (images.count == 0) return;
[self.gifView stopAnimating];
self.gifView.hidden = NO;
if (images.count == 1) // 单张图片
self.gifView.image = [images lastObject];
else // 多张图片
self.gifView.animationImages = images;
self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
[self.gifView startAnimating];
else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle)
[self.gifView stopAnimating];
self.gifView.hidden = YES;
十五、总结
15.1 涉及的iOS理论知识
(1) 运行时添加成员属性
在类目UIScrollView+MJRefresh
中运用运行时关联属性函数objc_setAssociatedObject
和objc_getAssociatedObject
对UIScrollView
视图控件添加了mj_header
、mj_footer
等属性。
#pragma mark - header
static const char MJRefreshHeaderKey = '\\0';
- (void)setMj_header:(MJRefreshHeader *)mj_header
if (mj_header != self.mj_header)
// 删除旧的,添加新的
[self.mj_header removeFromSuperview];
if (mj_header)
[self insertSubview:mj_header atIndex:0];
// 存储新的
objc_setAssociatedObject(self, &MJRefreshHeaderKey,
mj_header, OBJC_ASSOCIATION_RETAIN);
- (MJRefreshHeader *)mj_header
return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
#pragma mark - footer
static const char MJRefreshFooterKey = '\\0';
- (void)setMj_footer:(MJRefreshFooter *)mj_footer
if (mj_footer != self.mj_footer)
// 删除旧的,添加新的
[self.mj_footer removeFromSuperview];
if (mj_footer)
[self insertSubview:mj_footer atIndex:0];
// 存储新的
objc_setAssociatedObject(self, &MJRefreshFooterKey,
mj_footer, OBJC_ASSOCIATION_RETAIN);
- (MJRefreshFooter *)mj_footer
return objc_getAssociatedObject(self, &MJRefreshFooterKey);
15.2 涉及的编程思想
(1) 工厂方法及多态的运用
MJRefreshNormalHeader
是最下层的子类,headerWithRefreshingTarget
和headerWithRefreshingBlock
是最上层基类MJRefreshComponent
暴露出的静态快捷构造方法。把对象的创建下放到子类,此处明显用到了工厂方法。编译时类型mj_header
is classof MJRefreshHeader
,运行时类型MJRefreshNormalHeader
是其子类,此处是典型的多态特性的应用。
(2) 依赖倒置原则及分层实现的思想
即面向接口编码,而非面向实现编码。作者从最上层基类MJRefreshComponent
即暴露接口
#pragma mark - 交给子类们去实现
/** 初始化 */
- (void)prepare NS_REQUIRES_SUPER;
/** 摆放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 当scrollView的contentOffset发生改变的时候调用 */
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 当scrollView的contentSize发生改变的时候调用 */
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 当scrollView的拖拽状态发生改变的时候调用 */
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
具体实现:
层 | 实现 | 功用 |
---|---|---|
MJRefreshComponent | setState | 制定接口、回调、状态 |
MJRefreshHeader | setState prepare placeSubviews scrollViewContentOffsetDidChange | 设置默认值,处理手势下拉监听回调 |
MJRefreshStateHeader | setState prepare placeSubviews | 设置状态、日期文字 |
MJRefreshNormalHeader | setState prepare placeSubviews | 设置状态箭头、菊花标识 |
基类统一制定接口,客户端使用方便简捷;不同层次子类分别覆盖方法实现属于自己层内的功能,使逻辑清晰、功能独立,代码易扩展。
以上是关于iOS 李明杰 MJRefresh源码解析的主要内容,如果未能解决你的问题,请参考以下文章