MJRefresh 源码深度剖析:从 MJRefreshComponent 到自定义刷新控件全流程

MJRefresh 源码深度剖析:从 MJRefreshComponent 到自定义刷新控件全流程

【免费下载链接】MJRefresh An easy way to use pull-to-refresh. 【免费下载链接】MJRefresh 项目地址: https://gitcode.com/gh_mirrors/mj/MJRefresh

一、框架核心架构解析

MJRefresh作为iOS开发中最流行的下拉刷新框架,其核心架构采用了抽象基类+具体实现的设计模式。框架整体分为三层:基础组件层、核心功能层和自定义扩展层,这种分层设计保证了代码的高复用性和扩展性。

1.1 类继承关系概览

mermaid

核心基类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属性变化,实现了滑动事件的响应:

  1. 当用户滑动时,UIScrollView的contentOffset发生变化
  2. 触发scrollViewContentOffsetDidChange:方法
  3. 根据偏移量计算当前的拖拽比例和状态
  4. 根据状态变化更新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框架开发自定义刷新控件通常遵循以下步骤:

  1. 确定继承关系:根据需求选择合适的父类(通常是MJRefreshStateHeader或MJRefreshStateFooter)
  2. 重写prepare方法:初始化子控件和默认属性
  3. 重写placeSubviews方法:布局子控件
  4. 重写setState方法:处理状态变化时的UI更新
  5. 实现动画效果:根据状态变化实现对应的动画

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 性能优化建议

  1. 图片资源优化

    • 使用适当分辨率的图片,避免过大图片占用过多内存
    • 对于帧动画,合理控制帧数和图片大小
  2. 避免不必要的计算

    • 在scrollViewContentOffsetDidChange等高频调用方法中,减少复杂计算
    • 使用缓存存储计算结果
  3. 合理使用动画

    • 优先使用UIView动画,避免使用复杂的Core Animation动画
    • 控制动画持续时间,避免过长动画影响用户体验

5.2 常见问题解决方案

  1. 刷新控件位置异常

    • 检查UIScrollView的contentInset设置
    • 适当调整ignoredScrollViewContentInsetTop等属性
  2. CollectionView动画异常

    • 设置isCollectionViewAnimationBug属性为YES
    header.isCollectionViewAnimationBug = YES;
    
  3. 多语言适配

六、总结与展望

MJRefresh框架通过优雅的设计和完善的功能,成为了iOS下拉刷新领域的事实标准。其核心优势在于:

  1. 高度可定制化:提供了丰富的自定义接口,支持各种刷新样式
  2. 低耦合设计:通过分类方式为UIScrollView添加刷新功能,不侵入原有代码
  3. 完善的文档和示例:提供了详尽的Examples,位于Examples/MJRefreshExample

未来,随着SwiftUI的普及,MJRefresh可能会推出SwiftUI版本,进一步简化使用方式。同时,框架可以考虑增加更多交互效果和主题样式,满足不同App的设计需求。

通过本文的剖析,相信读者已经对MJRefresh的内部实现有了深入了解。框架的设计思想和实现方式,对于其他UI组件的开发也具有很好的借鉴意义。

【免费下载链接】MJRefresh An easy way to use pull-to-refresh. 【免费下载链接】MJRefresh 项目地址: https://gitcode.com/gh_mirrors/mj/MJRefresh

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值