持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情
上一篇文章的传送门: juejin.cn/post/715662…
MJRefreshHeader 与 MJRefreshFooter 都是继承于 MJRefreshComponent的,也是后续详细的 header、footer的基础父类。
关于头文件就不多做说明,已有足够的注释说明。
2、MJRefreshHeader 解析
首先可以看所有实现的方法:
- 两个构造方法
- 四个覆盖函数的方法
- 两个公共方法
构造方法(类方法)
#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;
}
构造方法的功能就是创建一个对象,并把回调或者方法写入对象当中,以便刷新的时候可以调用。
初始化处理
/// 初始化
- (void)prepare
{
[super prepare];
// 设置key
self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
// 设置高度
self.mj_h = MJRefreshHeaderHeight;
}
lastUpdatedTimeKey 是把 MJRefreshHeaderLastUpdatedTimeKey 直接写入其中,作为存储上一次刷新时间的 key。(只不过不知道为什么不直接使用 MJRefreshHeaderLastUpdatedTimeKey 而用 lastUpdatedTimeKey 作为中间替代呢?) mj_h 此时初始化为值 54.0,留作后面处理。(固定值,一直不变)
/// 摆放子控件
- (void)placeSubviews
{
[super placeSubviews];
// 设置y值(当自己的高度发生改变了,肯定要重新调整Y值,所以放到placeSubviews方法中设置y值)
self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}
这里已经有了注释了,就略过了。 ignoredScrollViewContentInsetTop 是 忽略多少scrollView的contentInset的top。
监听 scrollView 的滚动变化(核心功能)
/// 监听 scrollView 的 ContentOffset 发生变化之后的处理
- (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;
}
}
处理 refresh 的视图保留放在顶部, insetT 是为了设置 refresh 视图与顶部位置的偏移(UIEdgeInsets.top)
CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
这部分是判断现在的 scrollview 的 -y 是否大于 scrollview 的初始 insert 的顶部距离,并取其中最大的值。
insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
这部分是判断自身的高加上 scrollview 的初始 insert 的顶部距离与刚才算出来的最大值的大小,并选取最小值。 insetTDelta 是用来保存 insetT 与顶部距离的间隔。
// 跳转到下一个控制器时,contentInset可能会变
_scrollViewOriginalInset = self.scrollView.contentInset;
由于每次跳转都有可能改变 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;
}
此时一共分为三种状态:
- 正在拖拽时;
- 状态等于即将刷新 && 手松开;
- 手松开并且状态不等于即将刷新。
如果在拖拽状态: 则会在闲置状态与拖拽状态通过计算offset 是否超过普通或者即将刷新的临界点并进行切换。 如果在即将刷新 && 手松开: 则直接进行刷新。 如果手松开并且状态不等于即将刷新: 保存拖拽的百分比,用作后面处理。
状态的 setter 方法重写(核心功能)
/// 设置状态处理
- (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];
}];
});
}
}
其中需要注意 MJRefreshCheckState 是一个宏:
#define MJRefreshCheckState \
MJRefreshState oldState = self.state; \
if (state == oldState) return; \
[super setState:state];
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();
}
}];
}
如果当前状态是闲置且先前状态为刷新的话,则执行以下操作:
- 保存刷新时间;
- 动画恢复inset.如果开启了根据拖拽比例自动切换透明度,则透明度动画置0;
- 当前拖拽百分比置0,且执行结束刷新block。
公共方法处理
#pragma mark - 公共方法
- (void)endRefreshing
{
dispatch_async(dispatch_get_main_queue(), ^{
self.state = MJRefreshStateIdle;
});
}
- (NSDate *)lastUpdatedTime
{
return [[NSUserDefaults standardUserDefaults] objectForKey:self.lastUpdatedTimeKey];
}
给子类调用的,分别是设置结束刷新时候的状态和获取上次刷新时间。
3、MJRefreshFooter 解析
MJRefreshFooter 与 MJRefreshHeader 相比,只多了三个方法
/** 提示没有更多的数据 */
- (void)endRefreshingWithNoMoreData;
- (void)noticeNoMoreData MJRefreshDeprecated("使用endRefreshingWithNoMoreData");
/** 重置没有更多的数据(消除没有更多数据的状态) */
- (void)resetNoMoreData;
三个方法同属一种状态:MJRefreshStateNoMoreData 的不同变更。
公共方法
#pragma mark - 公共方法
- (void)endRefreshingWithNoMoreData
{
self.state = MJRefreshStateNoMoreData;
}
- (void)noticeNoMoreData
{
[self endRefreshingWithNoMoreData];
}
- (void)resetNoMoreData
{
self.state = MJRefreshStateIdle;
}
在 footer 对于自身的实现处理很少,除了构造方法与初始化之外,只实现了监听scrollView数据的变化的回调。其余的都在子类中实现。
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
if (newSuperview) {
// 监听scrollView数据的变化
if ([self.scrollView isKindOfClass:[UITableView class]] || [self.scrollView isKindOfClass:[UICollectionView class]]) {
[self.scrollView setMj_reloadDataBlock:^(NSInteger totalDataCount) {
if (self.isAutomaticallyHidden) {
self.hidden = (totalDataCount == 0);
}
}];
}
}
}
Mj_reloadDataBlock 在 reloaddate 执行之后执行,totalDataCount 是 tableview 的 row 的总数,只有在开启 AutomaticallyHidden 的时候才会执行(手动开启)。 至此,MJRefreshHeader 和 MJRefreshFooter 的解析也已经完成了。