前言
在MJRefresh源码阅读1——结构梳理中我们已经说了MJRefreshHeader
是整个控件的核心类,它完成了一个刷新控件应该有的所有逻辑和UI显示,它已经是个成型的,较简单的,麻雀虽小五脏俱全的刷新头header
。
说到一个成型的刷新头,
MJRefresh
它的核心逻辑应该是:当该header
添加到scrollView
上后,作者以scrollView
往下拉动到的不同偏移量,来相应地给header
定义了几种状态state
。scrollView
的偏移量contentOffset
引起header
的state
变化,而不同state
下要设置各自的显示样式。
我们直接来看MJRefreshHeader.m
文件。属性和方法概览如下图所示,因为该类代码较长,我们分段来分析。
可以看到在.m文件的extension
中定义了几个属性:显示上次刷新时间的标签updatedTimeLabel
;显示状态对应文字的标签stateLabel
;NSDate
类型的,表示上次刷新时间的updatedTime
;以及一个代表所有状态对应文字的字典stateTitles
。
然后.m
文件一开始便实现了其对应的getter
方法,在getter
方法里直接将其addSubView:
给header
了。
@interface MJRefreshHeader()
/** 显示上次刷新时间的标签 */
@property (weak, nonatomic) UILabel *updatedTimeLabel;
/** 上次刷新时间 */
@property (strong, nonatomic) NSDate *updatedTime;
/** 显示状态文字的标签 */
@property (weak, nonatomic) UILabel *stateLabel;
/** 所有状态对应的文字 */
@property (strong, nonatomic) NSMutableDictionary *stateTitles;
@end
@implementation MJRefreshHeader
#pragma mark - 懒加载
- (NSMutableDictionary *)stateTitles
{
if (!_stateTitles) {
self.stateTitles = [NSMutableDictionary dictionary];
}
return _stateTitles;
}
- (UILabel *)stateLabel
{
if (!_stateLabel) {
UILabel *stateLabel = [[UILabel alloc] init];
stateLabel.backgroundColor = [UIColor clearColor];
stateLabel.textAlignment = NSTextAlignmentCenter;
[self addSubview:_stateLabel = stateLabel];
}
return _stateLabel;
}
- (UILabel *)updatedTimeLabel
{
if (!_updatedTimeLabel) {
UILabel *updatedTimeLabel = [[UILabel alloc] init];
updatedTimeLabel.backgroundColor = [UIColor clearColor];
updatedTimeLabel.textAlignment = NSTextAlignmentCenter;
[self addSubview:_updatedTimeLabel = updatedTimeLabel];
}
return _updatedTimeLabel;
}
然后下来是几个“初始化方法”,“准备方法”。在它们几个方法中基本都是设置一些默认的属性。
#pragma mark - 初始化方法
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// 设置默认的dateKey
self.dateKey = MJRefreshHeaderUpdatedTimeKey;
// 设置为默认状态
self.state = MJRefreshHeaderStateIdle;
// 初始化文字
[self setTitle:MJRefreshHeaderStateIdleText forState:MJRefreshHeaderStateIdle];
[self setTitle:MJRefreshHeaderStatePullingText forState:MJRefreshHeaderStatePulling];
[self setTitle:MJRefreshHeaderStateRefreshingText forState:MJRefreshHeaderStateRefreshing];
}
return self;
}
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
if (newSuperview) {
self.mj_h = MJRefreshHeaderHeight;
}
}
- (void)drawRect:(CGRect)rect
{
if (self.state == MJRefreshHeaderStateWillRefresh) {
self.state = MJRefreshHeaderStateRefreshing;
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
// 设置自己的位置
self.mj_y = - self.mj_h;
// 2个标签都隐藏
if (self.stateHidden && self.updatedTimeHidden) return;
if (self.updatedTimeHidden) { // 显示状态
_stateLabel.frame = self.bounds;
} else if (self.stateHidden) { // 显示时间
self.updatedTimeLabel.frame = self.bounds;
} else { // 都显示
CGFloat stateH = self.mj_h * 0.55;
CGFloat stateW = self.mj_w;
// 1.状态标签
_stateLabel.frame = CGRectMake(0, 0, stateW, stateH);
// 2.时间标签
CGFloat updatedTimeY = stateH;
CGFloat updatedTimeH = self.mj_h - stateH;
CGFloat updatedTimeW = stateW;
self.updatedTimeLabel.frame = CGRectMake(0, updatedTimeY, updatedTimeW, updatedTimeH);
}
}
下面两个方法是对header
里上次刷新时间的处理。一个dateKey
对应一个updatedTime
,每个页面在刷新过程中只要变为refreshing
状态,便会存储该时刻的时间,是存储在userDefault
中的,以dateKey
为键,以updatedTime
为值。
- (void)setDateKey:(NSString *)dateKey
{
_dateKey = dateKey ? dateKey : MJRefreshHeaderUpdatedTimeKey;
self.updatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:_dateKey];
}
#pragma mark 设置最后的更新时间
- (void)setUpdatedTime:(NSDate *)updatedTime
{
_updatedTime = updatedTime;
if (updatedTime) {
[[NSUserDefaults standardUserDefaults] setObject:updatedTime forKey:self.dateKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
if (self.updatedTimeTitle) {
self.updatedTimeLabel.text = self.updatedTimeTitle(updatedTime);
return;
}
if (updatedTime) {
// 1.获得年月日
NSCalendar *calendar = [NSCalendar currentCalendar];
NSUInteger unitFlags = NSCalendarUnitYear| NSCalendarUnitMonth | NSCalendarUnitDay |NSCalendarUnitHour |NSCalendarUnitMinute;
NSDateComponents *cmp1 = [calendar components:unitFlags fromDate:updatedTime];
NSDateComponents *cmp2 = [calendar components:unitFlags fromDate:[NSDate date]];
// 2.格式化日期
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
if ([cmp1 day] == [cmp2 day]) { // 今天
formatter.dateFormat = @"今天 HH:mm";
} else if ([cmp1 year] == [cmp2 year]) { // 今年
formatter.dateFormat = @"MM-dd HH:mm";
} else {
formatter.dateFormat = @"yyyy-MM-dd HH:mm";
}
NSString *time = [formatter stringFromDate:updatedTime];
// 3.显示日期
self.updatedTimeLabel.text = [NSString stringWithFormat:@"最后更新:%@", time];
} else {
self.updatedTimeLabel.text = @"最后更新:无记录";
}
}
下面就到了最核心的地方了。我们在上一篇已经说了在MJRefreshComponent
类中已经以KVO的方式给scrollView
的contentOffset
属性添加了监听。只要contentOffset
属性发生变化便会执行下面的回调方法。代码中注释得很详细了,就不赘述了。
需要说明的是一开始我不明白_scrollViewOriginalInset
这个变量是什么意思——它就是代表scrollView
的原始contentInset
值。它不应当是0吗?其实有时不一定是0。
#pragma mark KVO属性监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// 遇到这些情况就直接返回
if (!self.userInteractionEnabled || self.alpha <= 0.01 || self.hidden || self.state == MJRefreshHeaderStateRefreshing) return;
// 根据contentOffset调整state
if ([keyPath isEqualToString:MJRefreshContentOffset]) {
[self adjustStateWithContentOffset];
}
}
#pragma mark 根据contentOffset调整state
- (void)adjustStateWithContentOffset
{
if (self.state != MJRefreshHeaderStateRefreshing) {
// 在刷新过程中,跳转到下一个控制器时,contentInset可能会变
_scrollViewOriginalInset = _scrollView.contentInset;
}
// 在刷新的 refreshing 状态,动态设置 content inset
if (self.state == MJRefreshHeaderStateRefreshing ) {
if(_scrollView.contentOffset.y >= -_scrollViewOriginalInset.top ) {
_scrollView.mj_insetT = _scrollViewOriginalInset.top;
} else {
_scrollView.mj_insetT = MIN(_scrollViewOriginalInset.top + self.mj_h,
_scrollViewOriginalInset.top - _scrollView.contentOffset.y);
}
return;
}
// 当前的contentOffset
CGFloat offsetY = _scrollView.mj_offsetY;
// 头部控件刚好出现的offsetY
CGFloat happenOffsetY = - _scrollViewOriginalInset.top;
// 如果是向上滚动到看不见头部控件,直接返回
if (offsetY >= happenOffsetY) return;
// 普通 和 即将刷新 的临界点
CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
if (_scrollView.isDragging)
{
self.pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
// 刚开始往下拉,拉到偏移量大于54时,状态变为pulling
if (self.state == MJRefreshHeaderStateIdle && offsetY < normal2pullingOffsetY) {
// #转为即将刷新状态
self.state = MJRefreshHeaderStatePulling;
// #当往下拉超过54后,往回推,推到54以上时状态由pulling变为idle
} else if (self.state == MJRefreshHeaderStatePulling && offsetY >= normal2pullingOffsetY) {
// 转为普通状态
self.state = MJRefreshHeaderStateIdle;
}
}
// #以下为松开手后
// 若松开手时此刻的状态是pulling,说明已往下拉过54的偏移量,则将其变为refreshing状态
else if (self.state == MJRefreshHeaderStatePulling) {// 即将刷新 && 手松开
self.pullingPercent = 1.0;
// 开始刷新
self.state = MJRefreshHeaderStateRefreshing;
} else {
self.pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
}
}
接下来就是几个提供给外部控制刷新状态的方法了。除了控件自身可以通过contentOffset
来切换状态外,外部调用者也可以调用这几个方法来切换header
的状态。包括最后一个方法判断header
是否正在刷新,即判断它当前的状态是否为MJRefreshHeaderStateRefreshing
。
- (void)beginRefreshing
{
if (self.window) {
self.state = MJRefreshHeaderStateRefreshing;
} else {
self.state = MJRefreshHeaderStateWillRefresh;
// 刷新(预防从另一个控制器回到这个控制器的情况,回来要重新刷新一下)
[self setNeedsDisplay];
}
}
- (void)endRefreshing
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.state = MJRefreshHeaderStateIdle;
});
}
- (BOOL)isRefreshing
{
return self.state == MJRefreshHeaderStateRefreshing;
}
接下来也是一个非常核心的方法,重写了state
的setter
方法。我们看看其在切换state
时都做了什么事。
第一个case
的意思是,外部调用了endRefreshing
方法,停止了刷新,状态由refreshing变为idle。此时首先记录并存储了当前的存储时间,然后将header
从顶部退出:其实是将scrollView
的contenInset
由原来的54设置为0。
第二个case
的意思是,开始刷新了,在开始refreshing
状态时,首先将header
的contentOffset
和contentInset
的值均设置为54。然后有block
回调,就调用执行block
回调,有SEL
回调,就调用执行SEL
回调。
- (void)setState:(MJRefreshHeaderState)state
{
if (_state == state) return;
// 旧状态
MJRefreshHeaderState oldState = _state;
// 赋值
_state = state;
// 设置状态文字
_stateLabel.text = _stateTitles[@(state)];
switch (state) {
case MJRefreshHeaderStateIdle: {
if (oldState == MJRefreshHeaderStateRefreshing) { // #当外部调用endRefreshing后,由refreshing状态变为idle状态
// 保存刷新时间
self.updatedTime = [NSDate date];
// 恢复inset和offset
[UIView animateWithDuration:MJRefreshSlowAnimationDuration delay:0.0 options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState animations:^{
// 修复top值不断累加
_scrollView.mj_insetT -= self.mj_h; // 刷新的header视图从顶部退出:其实是将scrollView的contenInset由原来的54设置为0
} completion:nil];
}
break;
}
case MJRefreshHeaderStateRefreshing: {
[UIView animateWithDuration:MJRefreshFastAnimationDuration delay:0.0 options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState animations:^{
// 增加滚动区域
CGFloat top = _scrollViewOriginalInset.top + self.mj_h;
_scrollView.mj_insetT = top;
// 设置滚动位置
_scrollView.mj_offsetY = - top;
} completion:^(BOOL finished) {
// 回调
if (self.refreshingBlock) {
self.refreshingBlock();
}
if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
msgSend(msgTarget(self.refreshingTarget), self.refreshingAction, self);
}
}];
break;
}
default:
break;
}
}
在该类的最后是下面几个功能方法。前两个是重写的父类的方法,后两个是重写的本类的两个属性的setter
方法,用来控制stateLabel
和updatedTimeLabel
的可见性,因为这两个可不可见会影响UI布局,所以在俩方法内都调用了setNeedsLayout
方法表示需要重绘,会再次执行一遍layoutSubviews
方法,重新调整一遍布局。
- (void)setTextColor:(UIColor *)textColor
{
[super setTextColor:textColor];
self.updatedTimeLabel.textColor = textColor;
self.stateLabel.textColor = textColor;
}
- (void)setFont:(UIFont *)font
{
[super setFont:font];
self.updatedTimeLabel.font = font;
self.stateLabel.font = font;
}
- (void)setStateHidden:(BOOL)stateHidden
{
_stateHidden = stateHidden;
self.stateLabel.hidden = stateHidden;
[self setNeedsLayout];
}
- (void)setUpdatedTimeHidden:(BOOL)updatedTimeHidden
{
_updatedTimeHidden = updatedTimeHidden;
self.updatedTimeLabel.hidden = updatedTimeHidden;
[self setNeedsLayout];
}
结尾
至此,MJRefresh
源码的逻辑基本梳理清楚了,能看清它是怎么实现的了。后面还会写一篇,整理一下它里面出现的一些值得掌握的知识点。