iOS源码解析iOS Developer转载

MJRefresh源码阅读1——结构梳理

2017-01-03  本文已影响575人  Wang66

前言

MJRefresh几乎是我们最常见的开源控件了。很有必要研究研究,但我们为节约时间,只研究这个控件的header部分,且只研究只有菊花和文字的普通样式的。footerheader类似,菊花型的刷新样式和Gif图片型的样式更是同理。


UIScrollView的分类:

tableView添加刷新header

[_tableView addLegendHeaderWithRefreshingTarget:self refreshingAction:@selector(pullRefresh)];

结束刷新:

[_tableView.header endRefreshing];

MJRefresh控件是如此友好,开发者使用只需要短短一两行代码。它是直接以UITableView的实例tableView对象来操作的,且结束刷新时,我们看到它是以tableViewheader属性调用endRefreshing方法的。我们知道UITableView是没有header属性的,它这个应该是通过UITableView分类的方式来动态添加地属性。这么做具有低侵入性的优点,不然也可以通过派生UITableView的方式来给其添加header属性,但这样做开发者在有刷新的地方就不能用UITableView,而一定要用其派生类了,这是不好的。自定义控件,让使用者用起来越简便越好。

MJRefresh确实是以分类的方式添加刷新headerfooter的,而且是UIScrollView的分类,因为该控件对UITableViewUICollectionView都支持刷新。

我们来看UIScollView分类UIScollView+MJRefresh的源码:

整体来看,在分类的头文件(UIScollView+MJRefresh.h)中总共有这么些东西:

屏幕快照 2017-01-03 下午5.31.33.png

可以看到所有的属性和方法分为headerfooter两部分,而且无论是添加header抑或footer的方法都提供了好几个接口。就以header来说添加刷新头就有addLegendHeader(传统样式刷新头)和addGifHeader(Gif样式刷新头)两类。而且单就以其中一类样式来看,也分别提供了两组回调方式不同的接口,即blockSEL两种方式。而且,在同一回调方式的一组里也有两个方法:一个接口有dateKey参数,一个没有这个参数。dateKey参数代表“时间的key”,默认的刷新控件有显示这个页面上次刷新是什么时候,MJRefresh内部维护了一个每个页面最后一次刷新时间的字典,该字典就以传入的dateKeykey(更正:每个页面上次刷新的时间在MJRefresh中没有以字典来维护,而是将每个界面上次刷新时间直接存入NSUserDefaults,正是以dateKey作为key来区分页面的)。

我们忽略其他代码,只看添加一个以SEL回调方式的legendHeader的代码。那么分类的头文件就是这样的:

@interface UIScrollView (MJRefresh)
#pragma mark - 访问下拉刷新控件
/** 下拉刷新控件 */
@property (strong, nonatomic, readonly) MJRefreshHeader *header;
/** 传统的下拉刷新控件 */
@property (nonatomic, readonly) MJRefreshLegendHeader *legendHeader;

...

/**
 * 添加一个传统的下拉刷新控件
 *
 * @param target 进入刷新状态就会自动调用target对象的action方法
 * @param action 进入刷新状态就会自动调用target对象的action方法
 */
- (MJRefreshLegendHeader *)addLegendHeaderWithRefreshingTarget:(id)target refreshingAction:(SEL)action;
/**
 * 添加一个传统的下拉刷新控件
 *
 * @param target    进入刷新状态就会自动调用target对象的action方法
 * @param action    进入刷新状态就会自动调用target对象的action方法
 * @param dateKey   用来记录刷新时间的key
 */
- (MJRefreshLegendHeader *)addLegendHeaderWithRefreshingTarget:(id)target refreshingAction:(SEL)action dateKey:(NSString *)dateKey;

/**
 * 移除下拉刷新控件
 */
- (void)removeHeader;

@end

同样的我们也忽略.m文件中其他代码:

@implementation UIScrollView (MJRefresh)
#pragma mark - 下拉刷新
- (MJRefreshLegendHeader *)addLegendHeaderWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    return [self addLegendHeaderWithRefreshingTarget:target refreshingAction:action dateKey:nil];
}

- (MJRefreshLegendHeader *)addLegendHeaderWithRefreshingTarget:(id)target refreshingAction:(SEL)action dateKey:(NSString *)dateKey
{
    MJRefreshLegendHeader *header = [self addLegendHeader];
    header.refreshingTarget = target;
    header.refreshingAction = action;
    header.dateKey = dateKey;
    return header;
}

- (MJRefreshLegendHeader *)addLegendHeader
{
    MJRefreshLegendHeader *header = [[MJRefreshLegendHeader alloc] init];
    self.header = header; // 将header赋值给了scrollView的属性header
    
    return header;
}

- (void)removeHeader
{
    self.header = nil;
}

#pragma mark - Property Methods
- (MJRefreshHeader *)header
{
    return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}

- (MJRefreshLegendHeader *)legendHeader
{
    if ([self.header isKindOfClass:[MJRefreshLegendHeader class]]) {
        return (MJRefreshLegendHeader *)self.header;
    }
    
    return nil;
}

static char MJRefreshHeaderKey;
- (void)setHeader:(MJRefreshHeader *)header
{
    if (header != self.header) {
        [self.header removeFromSuperview];
        
        [self willChangeValueForKey:@"header"];
        objc_setAssociatedObject(self, &MJRefreshHeaderKey,
                                 header,
                                 OBJC_ASSOCIATION_ASSIGN);
        [self didChangeValueForKey:@"header"];
        
        [self addSubview:header];
    }
}


#pragma mark - swizzle
+ (void)load
{
    Method method1 = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc"));
    Method method2 = class_getInstanceMethod([self class], @selector(deallocSwizzle));
    method_exchangeImplementations(method1, method2);
}

- (void)deallocSwizzle
{
    [self removeFooter];
    [self removeHeader];
    
    [self deallocSwizzle];
}

@end

可以看到在addLegendHeaderWithRefreshingTarget: refreshingAction: dateKey:方法里创建了MJRefreshLegendHeader类型的header,并将该方法的target,action,dateKey分别赋给header里与之对应的属性。
而且在创建header实例后将其赋给分类的属性header。我们知道分类中的属性是不会自动生成setter/getter方法的,要通过运行时(runtime)来实现。这个我们可以在上面的代码中看到,setHeader:方法里除了通过runtime来动态生成属性,还手动为其添加了观察(KVC和KVO的使用及原理),最后将header视图添加在了scrollView上。

在分类的最后,还以Method Swizzling的方式动态交换了deallocdeallocSwizzle俩方法的IMP(方法实现),使得在执行dealloc方法时实际上跑的是deallocSwizzle方法的实现,而在deallocSwizzle方法里移除了headerfooter

总结一下,这个分类主要功能是提供了一个添加header的很便捷的接口。在接口方法里创建了header实例,将其赋为分类的属性,并添加在scrollView上(addSubView:)。


header的派生类——MJRefreshLegendHeader:

既然上面在分类中创建了MJRefreshLegendHeader类型的实例,它是一个header的派生子类,表示传统样式的header。可以看到在它的头文件中没有暴露给外部任何东西,只能看到它是继承于MJRefreshHeader类的。

#import "MJRefreshHeader.h"

@interface MJRefreshLegendHeader : MJRefreshHeader

@end

再看它的.m文件中是怎么实现的:

#import "MJRefreshLegendHeader.h"
#import "MJRefreshConst.h"
#import "UIView+MJExtension.h"

@interface MJRefreshLegendHeader()
@property (nonatomic, weak) UIImageView *arrowImage;
@property (nonatomic, weak) UIActivityIndicatorView *activityView;
@end

@implementation MJRefreshLegendHeader
#pragma mark - 懒加载
- (UIImageView *)arrowImage
{
    if (!_arrowImage) {
        UIImageView *arrowImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:MJRefreshSrcName(@"arrow.png")]];
        [self addSubview:_arrowImage = arrowImage];
    }
    return _arrowImage;
}

- (UIActivityIndicatorView *)activityView
{
    if (!_activityView) {
        UIActivityIndicatorView *activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
        activityView.bounds = self.arrowImage.bounds;
        [self addSubview:_activityView = activityView];
    }
    return _activityView;
}

#pragma mark - 初始化
- (void)layoutSubviews
{
    [super layoutSubviews];
    
    // 箭头
    CGFloat arrowX = (self.stateHidden && self.updatedTimeHidden) ? self.mj_w * 0.5 : (self.mj_w * 0.5 - 100);
    self.arrowImage.center = CGPointMake(arrowX, self.mj_h * 0.5);
    
    // 指示器
    self.activityView.center = self.arrowImage.center;
}

#pragma mark - 公共方法
#pragma mark 设置状态
- (void)setState:(MJRefreshHeaderState)state
{
    if (self.state == state) return;
    
    // 旧状态
    MJRefreshHeaderState oldState = self.state;
    
    switch (state) {
        case MJRefreshHeaderStateIdle: {
            if (oldState == MJRefreshHeaderStateRefreshing) {
                self.arrowImage.transform = CGAffineTransformIdentity;
                
                [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                    self.activityView.alpha = 0.0;
                } completion:^(BOOL finished) {
                    self.arrowImage.alpha = 1.0;
                    self.activityView.alpha = 1.0;
                    [self.activityView stopAnimating];
                }];
            } else {
                [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                    self.arrowImage.transform = CGAffineTransformIdentity;
                }];
            }
            break;
        }
            
        case MJRefreshHeaderStatePulling: {
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                self.arrowImage.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
            }];
            break;
        }
            
        case MJRefreshHeaderStateRefreshing: {
            [self.activityView startAnimating];
            self.arrowImage.alpha = 0.0;
            break;
        }
            
        default:
            break;
    }
    
    // super里面有回调,应该在最后面调用
    [super setState:state];
}

@end

看了实现文件我们可以明白legendHeader是什么样的了:它在父类原有header的基础上新添了一个箭头图标和一个菊花视图。在下拉的过程中的不同状态下,箭头和菊花会做出相应的动画显示。这也就是整个MJRefreshLegendHeader实现文件所做的事。

可以看到,作者重写了arrowImageactivityView俩视图的getter方法来创建其实例,赋给属性,并addSubView:MJRefreshLegendHeader的实例上。
而且作者是在layoutSubviews这个方法中来设置调整俩视图的坐标位置的。

然后就是最核心的setState:方法了,它是一个重写的父类的setter方法,state是其父类MJRefreshHeader中的属性。这个方法的功能是当header在不同状态时,刷新头显示相应的动画。

说起状态,在其父类中定义了一个表示“状态”的枚举。

// 下拉刷新控件的状态
typedef enum {
    /** 普通闲置状态 */
    MJRefreshHeaderStateIdle = 1,
    /** 松开就可以进行刷新的状态 */
    MJRefreshHeaderStatePulling,
    /** 正在刷新中的状态 */
    MJRefreshHeaderStateRefreshing,
    /** 即将刷新的状态 */
    MJRefreshHeaderStateWillRefresh
} MJRefreshHeaderState;

MJRefreshHeaderStateIdle,即默认/闲置状态,图示:

屏幕快照 2017-01-03 22.36.31.png
MJRefreshHeaderStatePulling,即已经拉过临界值,松开手便会触发刷新,图示:
屏幕快照 2017-01-03 22.36.57.png
MJRefreshHeaderStateRefreshing,即已松开手,正在刷新,图示:
屏幕快照 2017-01-03 23.02.32.png

我们梳理一下下拉刷新过程的逻辑:
我们刚开始往下还没超过临界值时,如图一所示:箭头是向下的,文字为“下拉可以刷新”。对应的代码为第一个case中的else部分;
当我们继续往下拉超过临界值时,如图二所示:箭头旋转为向上,文字变为“松开可以刷新”。对应的代码为第二个case,即MJRefreshHeaderStatePulling部分;
当我们松开手后,如图三所示:箭头不见了,菊花出现并转动,文字变为“正在刷新数据中...”,并且请留意时间也更新了。对应的代码为第一个caseif部分。

整个过程包括了箭头和菊花的显示变化,文字的变化,以及时间的变化。但在该类的代码中只写了箭头和菊花的显示变化,文字和时间的变化是在其父类的setState:方法中完成的,在该方法的结尾它调用了父类的setState:方法,这我们都看到了。
该类是header的派生子类,它只处理了新派生出的功能,即箭头和菊花。而父类处理了文字和时间,那我们也大概能猜测出其父类MJRefreshHeaderheader是个什么模样:它没有菊花和箭头,但已有文字和时间,且能根据不同状态来变化文字和时间的显示。


header的基类——MJRefreshComponent:

从上面我们已知道派生类MJRefreshLegendHeader主要是在其父类的基础上添加了箭头和菊花的处理,其父类MJRefreshHeader已经是个成型的刷新头了。麻雀虽小,五脏俱全。它能处理状态变化的逻辑,虽然没有图标动画显示,但有文本跟随状态而变化。但MJRefreshHeader还不是header的尽头,它仍然继承于一个叫MJRefreshComponent的类,意为“刷新组件”,它定义和实现了刷新的最基本行为。我们来看看它的代码:

MJRefreshComponent.h文件:

@interface MJRefreshComponent : UIView
{
    UIEdgeInsets _scrollViewOriginalInset;
    __weak UIScrollView *_scrollView;

}

#pragma mark - 文字处理
/** 文字颜色 */
@property (strong, nonatomic) UIColor *textColor;
/** 字体大小 */
@property (strong, nonatomic) UIFont *font;

#pragma mark - 刷新处理
/** 正在刷新的回调 */
@property (copy, nonatomic) void (^refreshingBlock)();
/** 设置回调对象和回调方法 */
- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action;
@property (weak, nonatomic) id refreshingTarget;
@property (assign, nonatomic) SEL refreshingAction;
/** 进入刷新状态 */
- (void)beginRefreshing;
/** 结束刷新状态 */
- (void)endRefreshing;
/** 是否正在刷新 */
- (BOOL)isRefreshing;
@end

MJRefreshComponent.m文件:

@interface MJRefreshComponent()
/** 记录scrollView刚开始的inset */
@property (assign, nonatomic) UIEdgeInsets scrollViewOriginalInset;
/** 父控件 */
@property (weak, nonatomic) UIScrollView *scrollView;
@end

@implementation MJRefreshComponent
#pragma mark - 初始化
- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        // 基本属性
        self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
        self.backgroundColor = [UIColor clearColor];
        
        // 默认文字颜色和字体大小
        self.textColor = MJRefreshLabelTextColor;
        self.font = MJRefreshLabelFont;
    }
    return self;
}

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    // 旧的父控件
    [self.superview removeObserver:self forKeyPath:MJRefreshContentOffset context:nil];
    
    if (newSuperview) { // 新的父控件
        [newSuperview addObserver:self forKeyPath:MJRefreshContentOffset options:NSKeyValueObservingOptionNew context:nil];
        
        // 设置宽度
        self.mj_w = newSuperview.mj_w;
        // 设置位置
        self.mj_x = 0;
        
        // 记录UIScrollView
        self.scrollView = (UIScrollView *)newSuperview;
        // 设置永远支持垂直弹簧效果
        self.scrollView.alwaysBounceVertical = YES;
        // 记录UIScrollView最开始的contentInset
        self.scrollViewOriginalInset = self.scrollView.contentInset;
    }
}

#pragma mark - 公共方法
- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    self.refreshingTarget = target;
    self.refreshingAction = action;
}

- (void)beginRefreshing
{
    
}

- (void)endRefreshing
{
    
}

- (BOOL)isRefreshing {
    return NO;
}
@end

可以看到MJRefreshComponent类作为header的基类,它只是实现了一个header最基本最宏观的东西,具体逻辑是没有的。比如只有一些基本属性的设置,一些基本行为的方法,而且方法并未实现,等着让子类去重写实现。而且实现了一个最根本最核心的行为,就是当header被添加到scrollView上时,监听scrollViewcontentOffset属性,这是这个控件的最核心行为,一切状态变化,以及状态变化后引起的UI变化都由此“监听”而生。


结尾

本篇我们首先研究了提供添加header的接口的分类;然后研究了一个有箭头和菊花显示样式的,header的派生类;最后研究了其父类的父类MJRefreshComponent,而跳过了MJRefreshHeader类。之所以跳过该类,是因为该类是MJRefresh的核心类,它实现了一个成型的,可以运转的header,但本篇篇幅已经够长,因此会另起一篇。

上一篇下一篇

猜你喜欢

热点阅读