最近项目需要添加刷新交互,想着参考一下别人的源码再封装,然后就找了MJRefresh,然后记录下。
- 原理:
在
UIScrollView
可滚动区域的顶部上方或底部下方加一个UIView
,下拉时候监听偏移量,改变UIView
的内容显示。利用
UIScrollView
的contentInset
属性,在上下拉刷新时提供了显示UIView
的额外位置。
先看个例子:
tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
NSLog(@"下拉刷新");
}];
- 源码
- 首先创建分类使用
runtime
关联属性:
@implementation UIScrollView (MJRefresh)
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);//关联属性
}
- 然后初始化刷新控件:
@implementation MJRefreshHeader
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentAction)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;
}
- 控件通过继承来拓展功能:
@interface MJRefreshNormalHeader : MJRefreshStateHeader
@interface MJRefreshStateHeader : MJRefreshHeader
@interface MJRefreshHeader : MJRefreshComponent
@interface MJRefreshComponent : UIView
下面是继承结构与作用:
- 核心主要看
KVO
操作,当被添加进UIScrollView
时添加监听:
@implementation MJRefreshComponent
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 如果不是UIScrollView,不做任何事情
if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
// 旧的父控件移除监听
[self removeObservers];
if (newSuperview) { // 新的父控件
...
// 添加监听
[self addObservers];
}
}
/*
NSString *const MJRefreshKeyPathContentOffset = @"contentOffset";
NSString *const MJRefreshKeyPathContentSize = @"contentSize";
NSString *const MJRefreshKeyPathPanState = @"state";
*/
- (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];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(i18nDidChange) name:MJRefreshDidChangeLanguageNotification object:MJRefreshConfig.defaultConfig];
}
- (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];
}
}
//监听调用的方法由子类重写
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
- 当上下拉刷新时,监听
contentOffset
更新状态:
/** 刷新控件的状态 */
typedef NS_ENUM(NSInteger, MJRefreshState) {
/** 普通闲置状态 */
MJRefreshStateIdle = 1,
/** 松开就可以进行刷新的状态 */
MJRefreshStatePulling,
/** 正在刷新中的状态 */
MJRefreshStateRefreshing,
/** 即将刷新的状态 */
MJRefreshStateWillRefresh,
/** 所有数据加载完毕,没有更多的数据了 */
MJRefreshStateNoMoreData
};
@implementation MJRefreshHeader
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing状态
if (self.state == MJRefreshStateRefreshing) {
[self resetInset];
return;
}
// 跳转到下一个控制器时,contentInset可能会变
_scrollViewOriginalInset = self.scrollView.mj_inset;
// 当前的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
,松手后判断此前状态为MJRefreshStatePulling
时,代表要进行刷新操作,状态变为MJRefreshStateRefreshing
:
@implementation MJRefreshComponent
- (void)beginRefreshing
{
...
// 只要正在刷新,就完全显示
if (self.window) {
self.state = MJRefreshStateRefreshing;
} else {
// 预防正在刷新中时,调用本方法使得header inset回置失败
if (self.state != MJRefreshStateRefreshing) {
self.state = MJRefreshStateWillRefresh;
// 刷新(预防从另一个控制器回到这个控制器的情况,回来要重新刷新一下)
[self setNeedsDisplay];
}
}
}
- 当状态变化时,根据状态做事情:
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根据状态做事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;
[self headerEndingAction];
} else if (state == MJRefreshStateRefreshing) {
[self headerRefreshingAction];
}
}
- (void)headerEndingAction {
...
if (!self.isCollectionViewAnimationBug) {
// 恢复inset和offset
[UIView animateWithDuration:self.slowAnimationDuration animations:^{
self.scrollView.mj_insetT += self.insetTDelta;//修改contentInset,隐藏刷新控件
...
} completion:^(BOOL finished) { ... }];
return;
}
...
}
- (void)headerRefreshingAction {
if (!self.isCollectionViewAnimationBug) {
MJRefreshDispatchAsyncOnMainQueue({// 异步主线程执行,不强持有Self
[UIView animateWithDuration:self.fastAnimationDuration animations:^{
if (self.scrollView.panGestureRecognizer.state != UIGestureRecognizerStateCancelled) {
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
// 增加滚动区域top
self.scrollView.mj_insetT = top;//修改contentInset,显示出刷新控件
// 设置滚动位置
CGPoint offset = self.scrollView.contentOffset;
offset.y = -top;
[self.scrollView setContentOffset:offset animated:NO];//滚动到最顶部
}
} completion:^(BOOL finished) {
[self executeRefreshingCallback];//触发回调
}];
})
return;
}
...
}
@implementation UIScrollView (MJExtension)
- (void)setMj_insetT:(CGFloat)mj_insetT
{
UIEdgeInsets inset = self.contentInset;
inset.top = mj_insetT;
#ifdef __IPHONE_11_0
if (respondsToAdjustedContentInset_) {
inset.top -= (self.adjustedContentInset.top - self.contentInset.top);
}
#endif
self.contentInset = inset;
}
@implementation MJRefreshComponent
- (void)executeRefreshingCallback
{
MJRefreshDispatchAsyncOnMainQueue({
if (self.refreshingBlock) {
self.refreshingBlock();
}
if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
}
if (self.beginRefreshingCompletionBlock) {
self.beginRefreshingCompletionBlock();
}
})
}
到目前为止完成了刷新的调用,当刷新结束后,把状态变回MJRefreshStateIdle
就完成一个刷新操作了。
- 但是还有两个监听还没看到,监听
contentSize
用来更新footer
的位置:
@implementation MJRefreshBackFooter
- (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);
}
@implementation UIView (MJExtension)
- (void)setMj_y:(CGFloat)mj_y
{
CGRect frame = self.frame;
frame.origin.y = mj_y;
self.frame = frame;
}
监听scrollView.panGestureRecognizer
的state
只用来更新MJRefreshAutoFooter
这一个类的状态:
@implementation MJRefreshAutoFooter
- (void)scrollViewPanStateDidChange:(NSDictionary *)change
{
...
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;
}
}
简单来说,要封装刷新控件核心是监听
UIScrollView
的contentOffset
进行更新界面与触发回调,以及监听UIScrollView
的contentSize
来更新控件位置。