MJRefresh源码阅读

来源:互联网 发布:激光雷达避障算法 编辑:程序博客网 时间:2024/06/16 13:27

本文对应的MJRefresh版本是3.1.12

使用

MJRefresh的使用很简单,引入MJRefresh.h头文件,然后创建header或者footer赋值给tableView或者collectionView的mj_header或者mj_footer属性。设置好block或者target-action的回调就可以了。

接口也非常简单,如果没有特殊要求,提供的默认的样式已经很好了。同时MJRefresh内置的几个header和footer可以定制不同的样式,如果还不能满足需求,还可以自己写header和footer。

作者GitHub上的示例代码:

  // 下拉刷新    tableView.mj_header= [MJRefreshNormalHeader headerWithRefreshingBlock:^{        // 模拟延迟加载数据,因此2秒后才调用(真实开发中,可以移除这段gcd代码)        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{            // 结束刷新            [tableView.mj_header endRefreshing];        });    }];  // 上拉刷新    tableView.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{        // 模拟延迟加载数据,因此2秒后才调用(真实开发中,可以移除这段gcd代码)        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{            // 结束刷新            [tableView.mj_footer endRefreshing];        });    }];

原理

  1. 通过OC的runtime,在Category中给UIScrollView增加属性,mj_header和mj_footer。
  2. mj_header和mj_footer使用KVO观察UIScrollView的contentOffset属性,根据属性的值判断上拉和下拉的状态。
  3. mj_header和mj_footer的子类负责具体的显示样式和对上拉下拉状态的响应。

源码阅读

主要类图:
这里写图片描述

从引入的头文件开始,MJRefresh.h中又包含了好几个头文件,用途分为3种类型:
第一种是辅助类型的文件:

#import "UIScrollView+MJExtension.h"#import "UIView+MJExtension.h”

MJExtension的分类都是View上的辅助方法,为了写代码好看用的。比如UIScrollView(MJExtension)中的mj_offsetX属性,getter和setter方法是这样的:

- (CGFloat)mj_offsetX{    return self.contentOffset.x;}- (void)setMj_offsetY:(CGFloat)mj_offsetY{    CGPoint offset = self.contentOffset;    offset.y = mj_offsetY;    self.contentOffset = offset;}

所以这部分代码可以略过不看。

第二种是MJRefresh提供的header和footer

#import "MJRefreshNormalHeader.h"#import "MJRefreshGifHeader.h"#import "MJRefreshBackNormalFooter.h"...

第三种是UIScrollView的MJRefresh分类。

#import "UIScrollView+MJRefresh.h”

这个是阅读代码的入口。

UIScrollView的mj_header和mj_footer属性

在UIScrollView+MJRefresh.h中,给UIScrollView添加了两个属性:mj_header和mj_footer。使用runtime添加属性需要自己写getter和setter方法,在UIScrollView+MJRefresh.m文件中可以看到相关代码:

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);}

MJRefreshComponent。

MJRefresh对上拉和下拉的事件响应有两种方式,一种是回调block,另外一种是target-action。

/** 创建header */+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock;/** 创建header */+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action;

在什么情况下回执行回调呢?,从上边的类图可以看到,MJRefreshComponent是所有footer和header的基类,由它来处理UIScrollView和下来上拉刷新的所有状态,在MJRefreshComponent.h文件中定义了下拉上拉的状态:

/** 刷新控件的状态 */typedef NS_ENUM(NSInteger, MJRefreshState) {    /** 普通闲置状态 */    MJRefreshStateIdle = 1,    /** 松开就可以进行刷新的状态 */    MJRefreshStatePulling,    /** 正在刷新中的状态 */    MJRefreshStateRefreshing,    /** 即将刷新的状态 */    MJRefreshStateWillRefresh,    /** 所有数据加载完毕,没有更多的数据了 */    MJRefreshStateNoMoreData};

MJRefreshComponent在willMoveToSuperview中调用了addObserver函数对UIScrollView进行了监控:

- (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];}

观察了UIScrollView的contentOffset,contentSize,pan手势3个能影响下拉上拉状态的属性。
在对观察的响应中,MJRefreshComponent并没有真正的处理这些变化,而是调用了3个抽象方法,具体的处理交给了子类去做。

- (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{}

这3个方法并没有实现,是抽象方法。(在整个代码中看见只有autofooter类型的footer使用了scrollViewPanStateDidChange,并且因为响应了这个事件,还引入了bug,automaticallyRefresh=NO不起作用了,所以我使用的时候将这个监控去掉了)

MJRefreshComponent并没有对状态做判断,除了很确定的情况:在beginRefreshing和endRefreshing中设置了refreshing和idle状态。

MJRefreshComponent也并没有决定,在什么状态下调用刷新的回调,只留下一个executeRefreshingCallback方法供子类使用。

MJRefreshComponent并没有去创建或者布局界面,只是留了2个接口:prepare, placeSubviews
prepare创建过程中的一个钩子,在这里可以创建subview添加到控件中,
placeSubviews是layoutSubviews过程中的一个钩子,在这里可以布局subview。

MJRefreshHeader

MJRefreshHeader主要是处理了状态,即重写了这个方法:

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change

上边提到scrollViewContentOffsetDidChange, scrollViewContentSizeDidChange和scrollViewPanStateDidChange这3个方法决定着刷新控件的状态,和header的状态有关的就只是scrollViewContentOffsetDidChange。这个方法就是根据contentOffset的大小来决定当前的状态的。

到这层,header的状态跳转就能完全确定下来了,所以MJRefreshHeader的子类以及子类的子类都不需要处理下拉状态相关的事情了。

由于状态能确定了,所以根据状态变化的控件UI部分也能确定了,所以在setState(属性state的setter方法)中处理了一些和state相关的事宜,具体说来就是:

  • 保存了刷新时间
  • 恢复scrollView的inset
  • 设置整个控件的alpha值(可选)
  • 调用refreshing的回调(在基类MJRefreshComponent中定义的executeRefreshingCallback)

在MJRefreshHeader的头文件中有两个class方法:

/** 创建header */+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock;/** 创建header */+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action;

这两个方法可以实例化出MJRefreshHeader了,说明MJRefreshHeader已经是可实例化使用的header了,试一下确实可以使用下拉刷新,但是没有显示header的界面,因为MJRefreshHeader只处理了状态,但是没有添加任何subView显示。作者在下一层中才给出一个真正完备功能的header。

MJRefreshStateHeader

如果创建了一个MJRefreshStateHeader赋值给mj_header。可以看到下拉刷新显示当前状态文字,显示刷新时间显示文字。

由于有了MJRefreshComponent和MJRefreshHeader提供的基础设施,实现上述功能很简单:MJRefreshStateHeader添加了stateLabel和lastUpdateTimeLabel属性,并在重写的prepare方法中创建了它们。在placeSubviews方法中布局了它们。在setState中设置它们的文字。其他的事情不需要了

MJRefreshNormalHeader和MJRefreshGifHeader

这两个类是继承自MJRefreshStateHeader的,MJRefreshNormalHeader在stateLabel之前增加了一个表示状态的箭头图片和一个loading的转菊花。还是那3件事:

  • 在prepare中添加subview
  • 在placeSubviews中布局subview
  • 在setState中设置subView的状态(注意调用super,写在了宏中)

注:其实按照作者思路,应该在prepare中addSubview,但是arrowView的懒加载中调用了addSubview,而arrowView的getter是在placeSubviews中调用的,所以实际是在placeSubviews中addSubView的。

MJRefreshGifHeader是使用一个UIImageView来代替MJRefreshNormalHeader中的箭头和菊花,具体做法就是:

  • 增加一个stateImage,UIImageView类型(prepare中添加,placeSubviews中布局)
  • 给stateImage不同的状态设置不同的图片组
- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state 

在setState中根据不同的state播放不同的图片组。

重写MJRefreshComponent的setPullingPercent方法。能根据不同的下拉程度显示动态的效果。

Header的所有类就这些了,如果需求能被上述类Cover到,直接使用即可,一些可定制话的属性并没有介绍,比如可以选则隐藏stateLabel,或者改变stateLabel的文字颜色等。可以参考官网的Example。

如果上述类不能满足要求,那么应该从相应的层入手,比如官网的Example中有一个下来的吃包子的状态动画,就是继承自MJRefreshGifHeader,如果你的Header和状态无关,也可以继承自MJRefreshHeader。但是应该没有理由从MJRefreshComponent中继承,因为MJRefreshHeader处理了状态,而这些状态是确定的。

footer比header要复杂些。需求一般有:

  • 涉及到没有更多数据的显示,并在在上拉之后还要恢复刷加载更多的状态
  • 加载更多数据可以是到底部自动触发,也可以是有一个人为的操作才触发
  • 触发可以是上拉或者点击按钮
  • 数据不满一屏幕的时候footer是显示出来还是隐藏。

因为这些需求,MJRefreshFooter也不能确定下来footer的各个状态,因此在MJRefreshFooter中,仅仅做了几个简单属性的设置:高度,是否隐藏。对状态的控制就是在调用公开接口endRefreshingWithNoMoreData中将状态设置成为MJRefreshStateNoMoreData,在resetNoMoreData中设置状态为MJRefreshStateIdle。这些都是确定的行为。

根据上边的需求,作者将footer分为两种类型:MJRefreshAutoFooter和MJRefreshBackFooter。AutoFooter是只要scrollview滚动到底部控件都显示出来之后,就自动触发刷新,这个特性可以被禁止(将属性automaticallyRefresh设置为NO)。BackFooter是隐藏在屏幕之外的,要上拉才能看到,上拉到一定程度触发数据加载。上拉不到位就弹回屏幕下边。

可以看出backfooter的行为和header有些类似,所以对scrollViewContentOffsetDidChange的响应和header也是类似的,autofooter就简单了许多,只要区分现有的数据显示比一个屏幕长还是在一个屏幕之内:

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{    [super scrollViewContentOffsetDidChange:change];    if (self.state != MJRefreshStateIdle || !self.automaticallyRefresh || self.mj_y == 0) return;    if (_scrollView.mj_insetT + _scrollView.mj_contentH > _scrollView.mj_h) { // 内容超过一个屏幕        // 这里的_scrollView.mj_contentH替换掉self.mj_y更为合理        if (_scrollView.mj_offsetY >= _scrollView.mj_contentH - _scrollView.mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + _scrollView.mj_insetB - self.mj_h) {            // 防止手松开时连续调用            CGPoint old = [change[@"old"] CGPointValue];            CGPoint new = [change[@"new"] CGPointValue];            if (new.y <= old.y) return;            // 当底部刷新控件完全出现时,才刷新            [self beginRefreshing];        }    }else if(_scrollView.mj_offsetY > -_scrollView.mj_insetT + self.mj_h){            // 防止手松开时连续调用            CGPoint old = [change[@"old"] CGPointValue];            CGPoint new = [change[@"new"] CGPointValue];            if (new.y <= old.y) return;            // 当底部刷新控件完全出现时,才刷新            [self beginRefreshing];    }}

else if部分的代码是我加的,意思就是当数据不满一个屏幕的时候,如果上拉的高度大于了控件本身的高度,就触发刷新。加上这段代码之后就不需要响应scrollViewPanStateDidChange这个事件了。目前使用作者的test case测试还没有发现问题。

和header不同,footer的y坐标是要变化的,所以autofooter和backfooter都实现了- (void)scrollViewContentSizeDidChange:(NSDictionary *)change 方法,当scrollView的contentSize变化之后,重新调整y坐标。

MJRefreshAutoFooter和MJRefreshBackFooter在继承体系中的地位和MJRefreshHeader是一样的,因此,如果需要重写Footer,先确定是那种类型的Footer然后继承MJRefreshAutoFooter或者MJRefreshBackFooter就可以了。

如果没有特殊需求使用MJRefreshAutoNormalFooter或者MJRefreshBackNormalFooter就可以了。如果有和状态相关的动画需求使用MJRefreshAutoGifFooter和MJRefreshBackGifFooter即可。这些类的代码和Header几乎一样。

原创粉丝点击