iOS MJRefresh之 Header的实现
MJRefresh是Github上点赞次数最多的刷新控件,本文主要分析MJRefresh的实现原理,学习作者的思路,具体怎么使用可以去看MJRefresh得Demo。学习框架主要是学习作者的实现思路,本文主要是沿着框架的继承关系开始讲起。
1.为ScrollView添加MJRefreshHeader(UIScrollView+MJRefresh的分析)
MJRefresh定义了一个CategoryUIScrollView+MJRefresh.h
,为UIScrollView增加了1个属性:
/** 下拉刷新控件 */
@property (strong, nonatomic) MJRefreshHeader *mj_header;
我们只要为这个属性赋值就可以实现下拉刷新功能。
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(refreshListData)];
MJRefreshNormalHeader有两种初始化方法,两种的区别仅仅是回调方式不同,这两个方法在MJRefreshHeader中实现;
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock;
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action;
由于在类别中添加属性,并不能在编译期间自动添加成员变量、set和get方法(因为类的结构已经确定,在类别中再添加成员变量会影响已经添加的成员变量的存储。),所以我们要objc_setAssociatedObject
和objc_getAssociatedObject
的两个方法自己实现。
#pragma mark - header
static const char MJRefreshHeaderKey = '\0';
- (void)setMj_header:(MJRefreshHeader *)mj_header
{
if (mj_header != self.mj_header) {
// 删除旧的,添加新的
[self.mj_header removeFromSuperview];
[self insertSubview:mj_header atIndex:0];
// 存储新的
[self willChangeValueForKey:@"mj_header"]; // KVO
objc_setAssociatedObject(self, &MJRefreshHeaderKey,
mj_header, OBJC_ASSOCIATION_ASSIGN);
[self didChangeValueForKey:@"mj_header"]; // KVO
}
}
- (MJRefreshHeader *)mj_header
{
return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}
2.继承关系
继承结构图.pngMJRefreshComponent是Header和Footer的基类,所有我们从这个类开始分析。
3.MJRefreshComponent
从这个类我们可以知道,MJRefreshHeader的实现主要是利用KVO监听ScrollView的contentOffset,contentInset,contentSize的变化来确定ScrollView的刷新状态(MJRefreshState)。在- (void)setState:(MJRefreshState)state
中实现ScrollView的各个状态下的UI变化和调用回调方法。
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 如果不是UIScrollView,不做任何事情
if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
// 旧的父控件移除监听
[self removeObservers];
// 添加到父视图
if (newSuperview) { // 新的父控件
// 设置宽度
self.mj_w = newSuperview.mj_w;
// 设置位置
self.mj_x = -_scrollView.mj_insetL;
// 记录UIScrollView
_scrollView = (UIScrollView *)newSuperview;
// 设置永远支持垂直弹簧效果
_scrollView.alwaysBounceVertical = YES;
// 记录UIScrollView最开始的contentInset
_scrollViewOriginalInset = _scrollView.mj_inset;
// 添加监听
[self addObservers];
}
}
#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)removeObservers
{
[self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentOffset];
[self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentSize];
[self.pan removeObserver:self forKeyPath:MJRefreshKeyPathPanState];
self.pan = 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];
}
}
// 当ContentOffset变化的时候调用
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
// 当ContentSize变化的时候调用
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
- (void)willMoveToSuperview:(nullable UIView *)newSuperview;
这个方法在View添加到父视图(addSubView)和从父视图中移除(removeFromSuperView)都会调用。当newSuperview存在的时候,代表的是添加到父视图,此时添加KVO,并为每个属性设置了回调方法。 MJRefreshComponent的子类MJRefreshHeader实现- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change;
,根据ContentOffset的变化来确定ScrollView的刷新状态。所以,我们研究MJRefresh刷新的实现,就研究 MJRefreshComponent的各个子类在这个方法中的实现即可。
此外还定义了两个方法- (void)prepare;
和- (void)placeSubviews;
,都需要在子类中实现。在- (void)prepare;
方法中,主要是进行数据的初始化,- (void)placeSubviews;
中主要是实现控件的摆放,- (void)prepare;
先于- (void)placeSubviews;
执行。
4.MJRefreshHeader
在MJRefreshHeader中实现了- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
方法,确定了ScrollView的刷新状态,就是确定了这个属性@property (assign, nonatomic) MJRefreshState state;
的值。
分两种情况,一种是ScrollView正在刷新(MJRefreshStateRefreshing),通过修改ScrollView的contentInset,增加了contentInset.top的值,增加了MJRefreshHeader的高度,让MJRefreshHeader完全显示出来,以达到Hearder悬停效果;另一种是其他状态,此时来判断什么时候开始刷新。
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 此时ScrollView正在刷新
if (self.state == MJRefreshStateRefreshing) {
// 如果视图还没有添加到KeyWindow上,返回。
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.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;
}
}
MJRefreshComponent的每个子类都实现了- (void)setState:(MJRefreshState)state
这个方法。MJRefreshHeader实现了通过改变和恢复inset和offset来实现Scrollview的刷新效果。当state == MJRefreshStateIdle
的时候,恢复inset。当state == MJRefreshStateRefreshing
的时候,增加inset.top一个MJRefreshHeader的高度,并且滚动到顶部。
- (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;
// 设置滚动位置
CGPoint offset = self.scrollView.contentOffset;
offset.y = -top;
[self.scrollView setContentOffset:offset animated:NO];
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
});
}
}
这个类还定义了两个构造方法,在构造方法中保存了头部刷新的回调。
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock;
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action;
5.MJRefreshStateHeader
设置lastUpdatedTimeLabel和stateLabel的位置以及显示的内容。这个类非常简单,没有什么可讲的。这个类里的- (void)setState:(MJRefreshState)state;
实现的是这两个Label显示的内容。
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 设置状态文字
self.stateLabel.text = self.stateTitles[@(state)];
// 重新设置key(重新显示时间)
self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}
MJRefreshStateHeader.jpg
6. MJRefreshNormalHeader(MJRefreshGifHeader)
MJRefreshNormalHeader刷新的时候是一个菊花,MJRefreshGifHeader可以自定义MJRefreshHeader各个状态的动画,其实两个的实现思路大同小异,只是一个是UIActivityIndicatorView,一个是UIImageView,两者的位置都是相同的。剩下的就是动画效果的是实现了,这应该属于最基本的动画了,在这里我就不讲了,有不懂的可以自行百度。
此外还有一个箭头(arrowView),这个箭头就是UIImageView,它和UIActivityIndicatorView(Gif)交替展示,就是一个隐藏,一个就显示。这个类里的- (void)setState:(MJRefreshState)state;
实现的是arrowView和loadingView的动画效果。
- (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;
}
}
MJRefresh的分层非常清晰,一目了然,一些个功能的实现非常巧妙,而且大部分还有注释,这是了解UIScrollView以及刷新机制的非常好的一个框架。下一篇我将分析MJRefreshFooter。