Damon

宏愿纵未了 奋斗总不太晚

0%

MJRefresh源码浅析

MJRefresh 是一个功能强大的 iOS 上拉下拉刷新组件。有很多大厂App(抖音、快手、京东、喜马拉雅等)都在使用,所以是一个很值得学习的库。

原理

先简单说明上拉下拉刷新的原理再去详细分析 MJRefresh 框架结构以及具体实现。

header/footer的位置

首先 header/footer 是添加到 UIScrollView 上的,是它的子控件,比如 header 的高度是40,那么它的y值就是 -40 ,footer的y值就是 contentSizeHeight

下拉/上拉悬停

上拉或者下拉到一定程度,松手后就会进入刷新状态,这时候 header/footer 都在显示在可见范围悬停,这是通过设置 UIScrollView 的 contentInset.topcontentInset.bottom 来实现,刷新结束后,再重置回来。

设计

上面介绍了一些上拉下拉的原理,其实很简单, 接下去具体看一下 MJRefresh 是如何做的。

结构设计

MJRefreshComponent 是header和footer 的基类,实际都是使用最后一层的类。接下来具体看每个类的具体职责和实现。

MJRefreshComponent

这个类作为基类,主要的职责就是:

1. 声明刷新控件的状态,刷新的回调,交给子类去实现的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/** 刷新控件的状态 */
typedef NS_ENUM(NSInteger, MJRefreshState) {
/** 普通闲置状态 */
MJRefreshStateIdle = 1,
/** 松开就可以进行刷新的状态 */
MJRefreshStatePulling,
/** 正在刷新中的状态 */
MJRefreshStateRefreshing,
/** 即将刷新的状态 */
MJRefreshStateWillRefresh,
/** 所有数据加载完毕,没有更多的数据了 */
MJRefreshStateNoMoreData
};

/** 刷新用到的回调类型 */
typedef void (^MJRefreshComponentAction)(void);

#pragma mark - 交给子类们去实现
/** 初始化 */
- (void)prepare NS_REQUIRES_SUPER;
/** 摆放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 当scrollView的contentOffset发生改变的时候调用 */
- (void)scrollViewContentOffsetDidChange:(nullable NSDictionary *)change NS_REQUIRES_SUPER;
/** 当scrollView的contentSize发生改变的时候调用 */
- (void)scrollViewContentSizeDidChange:(nullable NSDictionary *)change NS_REQUIRES_SUPER;
/** 当scrollView的拖拽状态发生改变的时候调用 */
- (void)scrollViewPanStateDidChange:(nullable NSDictionary *)change NS_REQUIRES_SUPER;

2. 获取 UIScrollView,记录最开始的 contentInset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];

// 如果不是UIScrollView,不做任何事情
if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;

// 旧的父控件移除监听
[self removeObservers];

if (newSuperview) { // 新的父控件
// 记录UIScrollView
_scrollView = (UIScrollView *)newSuperview;

// 设置宽度
self.mj_w = _scrollView.mj_w;
// 设置位置
self.mj_x = -_scrollView.mj_insetL;

// 设置永远支持垂直弹簧效果
_scrollView.alwaysBounceVertical = YES;
// 记录UIScrollView最开始的contentInset
_scrollViewOriginalInset = _scrollView.mj_inset;

// 添加监听
[self addObservers];
}
}

记录 ContentInset 在 iOS11之后是获取 adjustedContentInset 属性。顺便看下设置 contentInset 的实现

1
2
3
4
5
6
7
8
9
10
11
- (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;
}

3. 添加监听

监听 contentOffset、contentSize、panGestureRecognizer的state。

1
2
3
4
5
6
7
8
- (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];
}

监听实现,具体处理交给子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (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{}

4. 开始刷新和结束刷新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (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)endRefreshing
{
MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateIdle;)
}

#pragma mark 是否正在刷新
- (BOOL)isRefreshing
{
return self.state == MJRefreshStateRefreshing || self.state == MJRefreshStateWillRefresh;
}

MJRefreshHeader

MJRefreshHeader 是所有 header 的父类,它的职责是:

1. 初始化设置

1
2
3
4
5
6
7
8
9
10
- (void)prepare
{
[super prepare];

// 设置key
self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;

// 设置高度
self.mj_h = MJRefreshHeaderHeight;
}

2. 设置y值

1
2
3
4
5
6
7
8
- (void)placeSubviews
{
[super placeSubviews];

// 设置y值(当自己的高度发生改变了,肯定要重新调整Y值,所以放到placeSubviews方法中设置y值)
self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}

ignoredScrollViewContentInsetTop 一般很少用到

3. 监听到 contentOffset 改变的具体实现

主要是根据偏移量来决定松手时候可刷新,还有就是悬停操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
- (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;
}
}

这个方法最主要是记录临界点 CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h; ,用 isDragging 属性来判断用户是否已经松手,如果没松手,就根据 contentOffset 来设置状态(提示松手可刷新或者取消),如果收松开并且状态是 MJRefreshStatePulling 就调用 beginRefreshing 开始刷新。header 并没有重写 beginRefreshing ,所以是调用基类的。

还有一个主要功能是悬停操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 在刷新的refreshing状态
if (self.state == MJRefreshStateRefreshing) {
[self resetInset];
return;
}

- (void)resetInset {
if (@available(iOS 11.0, *)) {
} else {
// 如果 iOS 10 及以下系统在刷新时, push 新的 VC, 等待刷新完成后回来, 会导致顶部 Insets.top 异常, 不能 resetInset, 检查一下这种特殊情况
if (!self.window) { 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.insetTDelta = _scrollViewOriginalInset.top - insetT;
// 避免 CollectionView 在使用根据 Autolayout 和 内容自动伸缩 Cell, 刷新时导致的 Layout 异常渲染问题
if (self.scrollView.mj_insetT != insetT) {
self.scrollView.mj_insetT = insetT;
}
}

和之前讲的一样,悬停的原理就是设置 contentInset.top

self.insetTDelta 这个属性可以理解为 header 的高度取反。

4. 状态切换

1
2
3
4
5
6
7
8
9
10
11
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState

// 根据状态做事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;
[self headerEndingAction];
} else if (state == MJRefreshStateRefreshing) {
[self headerRefreshingAction];
}

headerEndingActionheaderRefreshingAction 是对 contentOffset 做动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)headerRefreshingAction {
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
if (self.scrollView.panGestureRecognizer.state != UIGestureRecognizerStateCancelled) {
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
// 增加滚动区域top
self.scrollView.mj_insetT = top;
// 设置滚动位置
CGPoint offset = self.scrollView.contentOffset;
offset.y = -top;
[self.scrollView setContentOffset:offset animated:NO];
}
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
}

MJRefreshStateHeader

MJRefreshHeader 作为公共header,已经把原理篇的事情都做完了,设置y值、状态切换、悬停处理。

那么看看 MJRefreshStateHeader 的主要职责:

1. 初始化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (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];

}

初始化文字的原理就是用字典来存储每个状态对应的 title

2. 对 Label 进行布局

MJRefreshStateHeader 是负责状态Label和更新时间Label

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (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;
}
}
}

3. 切换状态

1
2
3
4
5
6
7
8
9
10
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState

// 设置状态文字
self.stateLabel.text = self.stateTitles[@(state)];

// 重新设置key(重新显示时间)
self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}

4. 设置上次刷新日期 Text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
- (void)setLastUpdatedTimeKey:(NSString *)lastUpdatedTimeKey
{
[super setLastUpdatedTimeKey:lastUpdatedTimeKey];

// 如果label隐藏了,就不用再处理
if (self.lastUpdatedTimeLabel.hidden) return;

NSDate *lastUpdatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:lastUpdatedTimeKey];

// 如果有block
if (self.lastUpdatedTimeText) {
self.lastUpdatedTimeLabel.text = self.lastUpdatedTimeText(lastUpdatedTime);
return;
}

if (lastUpdatedTime) {
// 1.获得年月日
NSCalendar *calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
NSUInteger unitFlags = NSCalendarUnitYear| NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute;
NSDateComponents *cmp1 = [calendar components:unitFlags fromDate:lastUpdatedTime];
NSDateComponents *cmp2 = [calendar components:unitFlags fromDate:[NSDate date]];

// 2.格式化日期
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
BOOL isToday = NO;
if ([cmp1 day] == [cmp2 day]) { // 今天
formatter.dateFormat = @" HH:mm";
isToday = YES;
} else if ([cmp1 year] == [cmp2 year]) { // 今年
formatter.dateFormat = @"MM-dd HH:mm";
} else {
formatter.dateFormat = @"yyyy-MM-dd HH:mm";
}
NSString *time = [formatter stringFromDate:lastUpdatedTime];

// 3.显示日期
self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@%@",
[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"",
time];
} else {
self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@",
[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
[NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];
}
}

有一个 lastUpdatedTimeText block 可以自定义内容。这个设置会在切换 state 的时候调用。

MJRefreshNormalHeader

1. 初始化

刷新样式的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)prepare
{
[super prepare];

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *)) {
_activityIndicatorViewStyle = UIActivityIndicatorViewStyleMedium;
return;
}
#endif

_activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
}

2. 对控件进行布局

MJRefreshNormalHeader 主要是负责 箭头和刷新小菊花的控件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (void)placeSubviews
{
[super placeSubviews];

// 箭头的中心点
CGFloat arrowCenterX = self.mj_w * 0.5;
if (!self.stateLabel.hidden) {
CGFloat stateWidth = self.stateLabel.mj_textWidth;
CGFloat timeWidth = 0.0;
if (!self.lastUpdatedTimeLabel.hidden) {
timeWidth = self.lastUpdatedTimeLabel.mj_textWidth;
}
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;
}

3. 状态切换处理

根据状态对 arrowViewloadView 进行动画和显示隐藏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- (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;
}
}

MJRefreshGifHeader

MJRefreshGifHeader 和 MJRefreshNormalHeader 的作用一样,都是进行刷新控件的布局和操作的,只不过 normal 是常用的箭头和菊花。gif就是用gif图片

1. 设置图片数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state 
{
if (images == nil) return;

self.stateImages[@(state)] = images;
self.stateDurations[@(state)] = @(duration);

/* 根据图片设置控件的高度 */
UIImage *image = [images firstObject];
if (image.size.height > self.mj_h) {
self.mj_h = image.size.height;
}
}

- (void)setImages:(NSArray *)images forState:(MJRefreshState)state
{
[self setImages:images duration:images.count * 0.1 forState:state];
}

2. 对 gifView 进行布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)placeSubviews
{
[super placeSubviews];

if (self.gifView.constraints.count) return;

self.gifView.frame = self.bounds;
if (self.stateLabel.hidden && self.lastUpdatedTimeLabel.hidden) {
self.gifView.contentMode = UIViewContentModeCenter;
} else {
self.gifView.contentMode = UIViewContentModeRight;

CGFloat stateWidth = self.stateLabel.mj_textWidth;
CGFloat timeWidth = 0.0;
if (!self.lastUpdatedTimeLabel.hidden) {
timeWidth = self.lastUpdatedTimeLabel.mj_textWidth;
}
CGFloat textWidth = MAX(stateWidth, timeWidth);
self.gifView.mj_w = self.mj_w * 0.5 - textWidth * 0.5 - self.labelLeftInset;
}
}

3. 状态切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState

// 根据状态做事情
if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {
NSArray *images = self.stateImages[@(state)];
if (images.count == 0) return;

[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 stopAnimating];
}
}

4. 根据拖拽的百分比来设置显示图片

1
2
3
4
5
6
7
8
9
10
11
12
- (void)setPullingPercent:(CGFloat)pullingPercent
{
[super setPullingPercent:pullingPercent];
NSArray *images = self.stateImages[@(MJRefreshStateIdle)];
if (self.state != MJRefreshStateIdle || images.count == 0) return;
// 停止动画
[self.gifView stopAnimating];
// 设置当前需要显示的图片
NSUInteger index = images.count * pullingPercent;
if (index >= images.count) index = images.count - 1;
self.gifView.image = images[index];
}

Header 的部分已经结束,最核心的就是 MJRefreshComponent MJRefreshHeader,这两个类把上拉刷新的核心功能都做完了,剩下的类就是根据状态显示对应的UI。

所以如果要自定义下拉刷新的UI可以直接继承 MJRefreshHeader

  1. prepare 方法进行初始化

  2. placeSubviews 方法进行布局

  3. 处理状态切换

    Footer 和 Header 的实现原理基本类似,就不多介绍了。总体来说上拉下拉刷新的原理还是比较简单的。通过看源码可以学到怎么分层,让每一个子类完成各自的事情,从而提高框架的可定制性。还有就是 UIScrollView 是比较复杂的控件,虽然原理简单但是实际上做出来的时候就是会出现不可思议的 bug, 可以学习如何追踪 bug 和解决 bug 的思路,MJRefresh 对 bug 的处理我没贴上去。感兴趣的可以自行学习。