持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情
MJRefresh 下载地址:github.com/CoderMJLee/… 关于 MJRefresh 的使用就不多做介绍,主要是想讲一下他的源码。 首先是 MJRefresh 的一个框架图:
从框架图中可以看到 MJRefreshComponent 是所有控件的基类,在从基类中分成header跟footer,而后再进行细分的。所以首先要分析的就是 MJRefreshComponent类
1、MJRefreshComponent 解析
MJRefreshComponent.h
MJRefreshComponent.h 中的属性与方法的解释都写的很详细了,这里我就介绍一下所引用的一些类
#import <UIKit/UIKit.h>
#import "MJRefreshConst.h"
#import "UIView+MJExtension.h"
#import "UIScrollView+MJExtension.h"
#import "UIScrollView+MJRefresh.h"
#import "NSBundle+MJRefresh.h"
类名 | 功能 |
---|---|
MJRefreshConst.h | MJ 的一部分固定常量 |
UIView+MJExtension.h | 改变、获取视图的宽、高、尺寸等 |
UIScrollView+MJExtension.h | 改变、获取滚动时图的 insert 跟 offset 的值 |
UIScrollView+MJRefresh.h | 获取与添加新的 footer 跟 header |
NSBundle+MJRefresh.h | 语言本地化跟箭头图标 |
这几个扩展里面的要点、难点不多,主要就是看如何使用category添加对象属性与KVO的手动刷新 其中有一个地方我比较感兴趣:
UIScrollView+MJRefresh.h 中的 mj_reloadData 方法
@implementation UITableView (MJRefresh)
+ (void)load
{
[self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
}
- (void)mj_reloadData
{
[self mj_reloadData];
[self executeReloadDataBlock];
}
@end
exchangeInstanceMethod1: method2: 是将 Method1 与 Method2 的方法进行交换 在这里面就是在调用 UITableView 的 reloadData 方法的时候变成调用此处的 mj_reloadData 而在方法中的
[self mj_reloadData];
反而调用的是 UITableView 的 reloadData 方法,相当于为 reloadData 方法加了方法, 而如果直接如下代码则会产生死循环
- (void)reloadData
{
[self reloadData];
[self executeReloadDataBlock];
}
UIScrollView+MJRefresh 中还有 mj_totalDataCount 与 (^mj_reloadDataBlock)(NSInteger totalDataCount) 属性 其主要作用就是在这是了 isAutomaticallyHidden 为 True 的情况下 mj_totalDataCount 为 0 的时候,将自动隐藏 footer,本人是基本没用到该方法,所以带过就好。
MJRefreshComponent.m
首先我们看一下 MJRefreshComponent.m 中的方法
初始化处理
首先在初始化的时候,要将属性设置为初始的状态
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// 准备工作
[self prepare];
// 默认是普通状态
self.state = MJRefreshStateIdle;
}
return self;
}
- (void)prepare
{
// 基本属性
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.backgroundColor = [UIColor clearColor];
}
这里主要是重置状态到普通、背景透明色与宽度按比例缩放。 layoutSubViews 主要是为了子类调用 placeSubViews,即在 addsubview 的时候要调用 placeSubViews;
- (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 = 0;
// 记录UIScrollView
_scrollView = (UIScrollView *)newSuperview;
// 设置永远支持垂直弹簧效果
_scrollView.alwaysBounceVertical = YES;
// 记录UIScrollView最开始的contentInset
_scrollViewOriginalInset = _scrollView.contentInset;
// 添加监听
[self addObservers];
}
}
_scrollViewOriginalInset 属性是为了在 Header 加载完毕或者在 BackFooter 加载完毕的时候可以返回到原来的位置; alwaysBounceVertical 属性是必备的,在设置弹簧效果关闭的时候,你会发现你无法使用 MJRefresh。
drawRect 可以按它字面意思理解,没什么好多讲的。
KVO监听
#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];
}
}
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
这一部分监听了三个属性
- scrollView 的 contentOffset;
- scrollView 的 contentSize;
- scrollView 的 panGestureRecognizer 的 状态。
所有的更改处理都交予子类, Component 当中只负责监听与调用方法。
接下来设置回调对象和回调方法只是属性赋值,而设置状态之后再调用 layoutSubViews 对子控件进行布局。
刷新状态处理
#pragma mark 进入刷新状态
- (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)beginRefreshingWithCompletionBlock:(void (^)())completionBlock
{
self.beginRefreshingCompletionBlock = completionBlock;
[self beginRefreshing];
}
#pragma mark 结束刷新状态
- (void)endRefreshing
{
self.state = MJRefreshStateIdle;
}
- (void)endRefreshingWithCompletionBlock:(void (^)())completionBlock
{
self.endRefreshingCompletionBlock = completionBlock;
[self endRefreshing];
}
#pragma mark 是否正在刷新
- (BOOL)isRefreshing
{
return self.state == MJRefreshStateRefreshing || self.state == MJRefreshStateWillRefresh;
}
这三块内容都是刷新相关的、外界调用的,主要的功能就是设置开始刷新回调,结束刷新回调; 在开始刷新的方法中,判断如果已经放置在window上,则开始刷新,否则将状态置为准备刷新,并刷新界面(刷新就如同注释所说,如果是pop或者dismiss会到改控制器,则此时不在window中也不会再次调用,所以需要手动刷新);
透明度设置
#pragma mark 自动切换透明度
- (void)setAutoChangeAlpha:(BOOL)autoChangeAlpha
{
self.automaticallyChangeAlpha = autoChangeAlpha;
}
- (BOOL)isAutoChangeAlpha
{
return self.isAutomaticallyChangeAlpha;
}
- (void)setAutomaticallyChangeAlpha:(BOOL)automaticallyChangeAlpha
{
_automaticallyChangeAlpha = automaticallyChangeAlpha;
if (self.isRefreshing) return;
if (automaticallyChangeAlpha) {
self.alpha = self.pullingPercent;
} else {
self.alpha = 1.0;
}
}
#pragma mark 根据拖拽进度设置透明度
- (void)setPullingPercent:(CGFloat)pullingPercent
{
_pullingPercent = pullingPercent;
if (self.isRefreshing) return;
if (self.isAutomaticallyChangeAlpha) {
self.alpha = pullingPercent;
}
}
这部分就很简单了,就是赋值、设置透明度,其中拉拽的百分比是通过子类获取的,在 .h 文件中有说明。
调用刷新回调
#pragma mark - 内部方法
- (void)executeRefreshingCallback
{
dispatch_async(dispatch_get_main_queue(), ^{
if (self.refreshingBlock) {
self.refreshingBlock();
}
if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
}
if (self.beginRefreshingCompletionBlock) {
self.beginRefreshingCompletionBlock();
}
});
}
子类在结束刷新动画之后,调用刷新结束的回调,MJRefreshMsgSend 是 objc_msgSend 方法的格式化。是为了动态发送请求调用
// 运行时objc_msgSend
#define MJRefreshMsgSend(...) ((void (*)(void *, SEL, UIView *))objc_msgSend)(__VA_ARGS__)
#define MJRefreshMsgTarget(target) (__bridge void *)(target)
对这部分不了解的可以去查一下 Runtime。