MJRefresh 源码深度剖析:从 MJRefreshComponent 到自定义刷新控件全流程
一、框架核心架构解析
MJRefresh作为iOS开发中最流行的下拉刷新框架,其核心架构采用了抽象基类+具体实现的设计模式。框架整体分为三层:基础组件层、核心功能层和自定义扩展层,这种分层设计保证了代码的高复用性和扩展性。
1.1 类继承关系概览
核心基类MJRefreshComponent定义了所有刷新控件的公共属性和方法,位于MJRefresh/Base/MJRefreshComponent.h。该类封装了与UIScrollView的交互逻辑、状态管理和动画控制,是整个框架的基石。
二、MJRefreshComponent:刷新控件的基石
2.1 核心属性解析
MJRefreshComponent作为所有刷新控件的基类,定义了以下关键属性:
@interface MJRefreshComponent : UIView {
/** 记录scrollView刚开始的inset */
UIEdgeInsets _scrollViewOriginalInset;
/** 父控件 */
__weak UIScrollView *_scrollView;
}
/** 正在刷新的回调 */
@property (copy, nonatomic, nullable) MJRefreshComponentAction refreshingBlock;
/** 回调对象 */
@property (weak, nonatomic) id refreshingTarget;
/** 回调方法 */
@property (assign, nonatomic) SEL refreshingAction;
/** 刷新状态 */
@property (assign, nonatomic) MJRefreshState state;
这些属性实现了刷新控件与UIScrollView的绑定、刷新事件的回调机制以及状态管理,是所有刷新控件的基础。
2.2 状态管理机制
框架定义了五种刷新状态,通过状态机模式实现状态之间的转换:
typedef NS_ENUM(NSInteger, MJRefreshState) {
/** 普通闲置状态 */
MJRefreshStateIdle = 1,
/** 松开就可以进行刷新的状态 */
MJRefreshStatePulling,
/** 正在刷新中的状态 */
MJRefreshStateRefreshing,
/** 即将刷新的状态 */
MJRefreshStateWillRefresh,
/** 所有数据加载完毕,没有更多的数据了 */
MJRefreshStateNoMoreData
};
状态的转换通过setState:方法实现,该方法会触发对应的UI更新和事件回调。状态管理的核心代码位于MJRefresh/Base/MJRefreshComponent.m,通过重写setter方法实现状态变更时的逻辑处理。
2.3 事件响应流程
MJRefreshComponent通过KVO监听UIScrollView的contentOffset属性变化,实现了滑动事件的响应:
- 当用户滑动时,UIScrollView的
contentOffset发生变化 - 触发
scrollViewContentOffsetDidChange:方法 - 根据偏移量计算当前的拖拽比例和状态
- 根据状态变化更新UI和触发回调
关键代码实现如下:
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change {
[super scrollViewContentOffsetDidChange:change];
// 如果正在刷新,直接返回
if (self.state == MJRefreshStateRefreshing) return;
// 获取当前contentOffset
CGPoint offset = self.scrollView.contentOffset;
// 计算头部控件的可见高度
CGFloat visibleHeight = -offset.y + self.scrollViewOriginalInset.top;
if (visibleHeight <= 0) return; // 如果没有可见高度,直接返回
// 根据拖拽距离计算拖拽比例
self.pullingPercent = visibleHeight / self.mj_h;
// 判断状态
if (self.scrollView.isDragging) {
// 正在拖拽
if (self.state == MJRefreshStateIdle && visibleHeight > self.mj_h) {
// 拖拽距离大于控件高度,进入下拉状态
self.state = MJRefreshStatePulling;
} else if (self.state == MJRefreshStatePulling && visibleHeight <= self.mj_h) {
// 拖拽距离小于等于控件高度,恢复闲置状态
self.state = MJRefreshStateIdle;
}
} else if (self.state == MJRefreshStatePulling) {
// 停止拖拽,并且是下拉状态,进入刷新状态
self.state = MJRefreshStateRefreshing;
}
}
三、核心功能实现:Header、Footer与Trailer
3.1 下拉刷新(MJRefreshHeader)
MJRefreshHeader继承自MJRefreshComponent,专门用于实现下拉刷新功能,位于MJRefresh/Base/MJRefreshHeader.h。它扩展了一些头部刷新特有的属性:
@interface MJRefreshHeader : MJRefreshComponent
/** 这个key用来存储上一次下拉刷新成功的时间 */
@property (copy, nonatomic) NSString *lastUpdatedTimeKey;
/** 上一次下拉刷新成功的时间 */
@property (strong, nonatomic, readonly, nullable) NSDate *lastUpdatedTime;
/** 忽略多少scrollView的contentInset的top */
@property (assign, nonatomic) CGFloat ignoredScrollViewContentInsetTop;
@end
其中,lastUpdatedTimeKey用于存储上次刷新时间,方便在UI上显示"最后更新时间",这是很多App常见的需求。
3.2 状态式头部(MJRefreshStateHeader)
MJRefreshStateHeader进一步扩展了MJRefreshHeader,增加了状态文本显示功能,位于MJRefresh/Custom/Header/MJRefreshStateHeader.h。它提供了状态文本设置的接口:
/** 设置state状态下的文字 */
- (instancetype)setTitle:(NSString *)title forState:(MJRefreshState)state;
通过这个方法,可以为不同的刷新状态设置不同的提示文字,例如:
// 设置状态文字
[self setTitle:@"下拉可以刷新" forState:MJRefreshStateIdle];
[self setTitle:@"松开立即刷新" forState:MJRefreshStatePulling];
[self setTitle:@"正在刷新..." forState:MJRefreshStateRefreshing];
3.3 经典样式实现(MJRefreshNormalHeader)
MJRefreshNormalHeader是框架提供的默认刷新头部实现,位于MJRefresh/Custom/Header/MJRefreshNormalHeader.h。它包含一个箭头图标和一个加载指示器:
@interface MJRefreshNormalHeader : MJRefreshStateHeader
@property (weak, nonatomic, readonly) UIImageView *arrowView;
@property (weak, nonatomic, readonly) UIActivityIndicatorView *loadingView;
@end
该类实现了经典的下拉刷新动画:下拉时箭头旋转,释放后显示loading动画。核心动画实现如下:
- (void)setState:(MJRefreshState)state {
MJRefreshCheckState;
// 根据状态设置箭头和loading
if (state == MJRefreshStateIdle) {
if (oldState == MJRefreshStateRefreshing) {
// 刷新结束,箭头恢复状态并执行回弹动画
self.arrowView.transform = CGAffineTransformIdentity;
[UIView animateWithDuration:self.slowAnimationDuration animations:^{
self.scrollView.contentInset = self.scrollViewOriginalInset;
} completion:^(BOOL finished) {
self.pullingPercent = 0.0;
}];
} else {
// 恢复箭头方向
[UIView animateWithDuration:0.25 animations:^{
self.arrowView.transform = CGAffineTransformIdentity;
}];
}
[self.loadingView stopAnimating];
} else if (state == MJRefreshStatePulling) {
// 下拉状态,箭头旋转180度
[UIView animateWithDuration:0.25 animations:^{
self.arrowView.transform = CGAffineTransformMakeRotation(M_PI);
}];
[self.loadingView stopAnimating];
} else if (state == MJRefreshStateRefreshing) {
// 刷新状态,开始loading动画
[self.loadingView startAnimating];
// 执行刷新动画
[UIView animateWithDuration:self.fastAnimationDuration animations:^{
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
self.scrollView.mj_insetT = top;
self.scrollView.contentOffset = CGPointMake(0, -top);
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
}
}
四、自定义刷新控件全流程
4.1 自定义控件开发步骤
基于MJRefresh框架开发自定义刷新控件通常遵循以下步骤:
- 确定继承关系:根据需求选择合适的父类(通常是MJRefreshStateHeader或MJRefreshStateFooter)
- 重写prepare方法:初始化子控件和默认属性
- 重写placeSubviews方法:布局子控件
- 重写setState方法:处理状态变化时的UI更新
- 实现动画效果:根据状态变化实现对应的动画
4.2 案例:自定义动画刷新控件
下面以Examples中的MJCustomGifHeader为例,介绍如何实现一个带动画效果的自定义刷新控件。该控件位于Examples/MJRefreshExample/MJRefreshExample/Classes/DIY/MJCustomGifHeader.h。
步骤1:定义自定义头部类
#import "MJRefreshGifHeader.h"
@interface MJCustomGifHeader : MJRefreshGifHeader
@end
步骤2:重写prepare方法初始化
- (void)prepare {
[super prepare];
// 设置控件高度
self.mj_h = 100;
// 加载普通状态图片
NSMutableArray *idleImages = [NSMutableArray array];
for (int i = 1; i <= 60; i++) {
UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_anim__000%zd", i]];
[idleImages addObject:image];
}
[self setImages:idleImages forState:MJRefreshStateIdle];
// 加载下拉状态图片
NSMutableArray *pullingImages = [NSMutableArray array];
for (int i = 1; i <= 3; i++) {
UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_loading_0%zd", i]];
[pullingImages addObject:image];
}
[self setImages:pullingImages forState:MJRefreshStatePulling];
// 加载刷新状态图片
NSMutableArray *refreshingImages = [NSMutableArray array];
for (int i = 1; i <= 3; i++) {
UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_loading_0%zd", i]];
[refreshingImages addObject:image];
}
[self setImages:refreshingImages forState:MJRefreshStateRefreshing];
}
步骤3:重写placeSubviews方法布局
- (void)placeSubviews {
[super placeSubviews];
// 调整gif图片位置
self.gifView.contentMode = UIViewContentModeCenter;
self.gifView.mj_w = self.mj_w;
self.gifView.mj_h = self.mj_h;
// 隐藏状态标签和时间标签
self.stateLabel.hidden = YES;
self.lastUpdatedTimeLabel.hidden = YES;
}
步骤4:使用自定义控件
// 创建自定义头部
MJCustomGifHeader *header = [MJCustomGifHeader headerWithRefreshingTarget:self refreshingAction:@selector(refreshData)];
self.tableView.mj_header = header;
效果展示
该自定义控件实现了类似特定App的下拉刷新效果,下拉时显示帧动画,释放后显示加载动画:
五、框架最佳实践与性能优化
5.1 性能优化建议
-
图片资源优化:
- 使用适当分辨率的图片,避免过大图片占用过多内存
- 对于帧动画,合理控制帧数和图片大小
-
避免不必要的计算:
- 在scrollViewContentOffsetDidChange等高频调用方法中,减少复杂计算
- 使用缓存存储计算结果
-
合理使用动画:
- 优先使用UIView动画,避免使用复杂的Core Animation动画
- 控制动画持续时间,避免过长动画影响用户体验
5.2 常见问题解决方案
-
刷新控件位置异常:
- 检查UIScrollView的contentInset设置
- 适当调整ignoredScrollViewContentInsetTop等属性
-
CollectionView动画异常:
- 设置isCollectionViewAnimationBug属性为YES
header.isCollectionViewAnimationBug = YES; -
多语言适配:
- 使用框架提供的多语言支持,位于MJRefresh/MJRefresh.bundle/en.lproj/Localizable.strings
- 自定义多语言可修改CustomLanguages.bundle,位于Examples/MJRefreshExample/MJRefreshExample/Classes/i18n/CustomLanguages.bundle
六、总结与展望
MJRefresh框架通过优雅的设计和完善的功能,成为了iOS下拉刷新领域的事实标准。其核心优势在于:
- 高度可定制化:提供了丰富的自定义接口,支持各种刷新样式
- 低耦合设计:通过分类方式为UIScrollView添加刷新功能,不侵入原有代码
- 完善的文档和示例:提供了详尽的Examples,位于Examples/MJRefreshExample
未来,随着SwiftUI的普及,MJRefresh可能会推出SwiftUI版本,进一步简化使用方式。同时,框架可以考虑增加更多交互效果和主题样式,满足不同App的设计需求。
通过本文的剖析,相信读者已经对MJRefresh的内部实现有了深入了解。框架的设计思想和实现方式,对于其他UI组件的开发也具有很好的借鉴意义。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考




