阅读MJRefresh源码

该框架的结构设计得很清晰,使用一个基类MJRefreshComponent来做一些基本的设定,然后通过继承的方式,让MJRefreshHeader和MJRefreshFooter分别具备下拉刷新和上拉加载的功能。从继承结构来看可以分为三层,具体可以从下面的图里看出来:

![image.png](https://upload-images.jianshu.io/upload_images/1303053-58fd0991867d57dd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

MJRefreshComponent


这个类作为该控件的基类,涵盖了基类所具备的一些:状态,回调block等,大致分成下面这5种职能:

1.声明控件的所有状态

/** 刷新控件的状态 */

typedef NS_ENUM(NSInteger, MJRefreshState) {

    /** 普通闲置状态 */

    MJRefreshStateIdle = 1,

   /** 松开就可以进行刷新的状态 */

    MJRefreshStatePulling,

    /** 正在刷新中的状态 */

    MJRefreshStateRefreshing,

    /** 即将刷新的状态 */

    MJRefreshStateWillRefresh,

    /** 所有数据加载完毕,没有更多的数据了 */

    MJRefreshStateNoMoreData

};

2.声明控件的回调函数

/** 进入刷新状态的回调 */

typedef void (^MJRefreshComponentRefreshingBlock)();

/** 开始刷新后的回调(进入刷新状态后的回调) */

typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)();

/** 结束刷新后的回调 */

typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)();

3.添加监听事件

- (void)addObservers

{

    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];//contentOffset属性

    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];//contentSize属性

    self.pan = self.scrollView.panGestureRecognizer;

    [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];//UIPanGestureRecognizer 的state属性

}

对于监听的处理:

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

}

}



4.提供刷新,停止刷新接口

#pragma mark 进入刷新状态

- (void)beginRefreshingWithCompletionBlock:(void (^)())completionBlock

{

    self.beginRefreshingCompletionBlock = completionBlock;


    [self beginRefreshing];

}

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

        }

    }

}

#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;

}

5.提供子类需要实现的方法

#pragma mark - 交给子类们去实现

/** 初始化 */

- (void)prepare NS_REQUIRES_SUPER;

/** 摆放子控件frame */

- (void)placeSubviews NS_REQUIRES_SUPER;

/** 当scrollView的contentOffset发生改变的时候调用 */

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;

/** 当scrollView的contentSize发生改变的时候调用 */

- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;

/** 当scrollView的拖拽状态发生改变的时候调用 */

- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;

从上面等结构图可以看出,紧接着这个基类,下面分为MJRefreshHeader和MJRefreshFooter,这里顺着MJRefreshHeader这个分支向下展开:

MJRefreshHeader


MJRefreshHeader继承于MJRefreshComponent,它做了这几件事:

1.初始化。

初始化有两种方法:

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock

{

    MJRefreshHeader *cmp = [[self alloc] init];

    //传入block

    cmp.refreshingBlock = refreshingBlock;

    return cmp;

}

+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action

{

    MJRefreshHeader *cmp = [[self alloc] init];

    //设置self.refreshingTarget 和 self.refreshingAction

    [cmp setRefreshingTarget:target refreshingAction:action];

    return cmp;

}

2.设置header高度。

通过重写prepare方法来设置header的高度:

- (void)prepare

{

    [super prepare];


    // 设置用于在NSUserDefaults里存储时间的key

    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;


    // 设置header的高度

    self.mj_h = MJRefreshHeaderHeight;

}


3.重新调整y值。

通过重写placeSubviews方法来重新调整y值:

- (void)placeSubviews

{

    [super placeSubviews];

   // 设置y值(当自己的高度发生改变了,肯定要重新调整Y值,所以放到placeSubviews方法中设置y值)

    self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;

    //self.ignoredScrollViewContentInsetTop 如果是10,那么就向上移动10

}

4.根据contentOffset的变化,来切换状态(默认状态,可以刷新的状态,正在刷新的状态),实现方法是:scrollViewContentOffsetDidChange:。

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change

{

    [super scrollViewContentOffsetDidChange:change];

    // 正在刷新的状态

    if (self.state == MJRefreshStateRefreshing) {

        if (self.window == nil) return;

        //- self.scrollView.mj_offsetY:-(-54-64)= 118 : 刷新的时候,偏移量是不动的。偏移量 = 状态栏 + 导航栏 + header的高度

        //_scrollViewOriginalInset.top:64 (状态栏 + 导航栏)

        //insetT 取二者之间大的那一个

        CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;

        //118

        insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;

        //设置contentInset

        self.scrollView.mj_insetT = insetT;

        // 记录刷新的时候的偏移量 -54 = 64 - 118

        self.insetTDelta = _scrollViewOriginalInset.top - insetT;

        return;

    }

    // 跳转到下一个控制器时,contentInset可能会变

    _scrollViewOriginalInset = self.scrollView.contentInset;

    // 记录当前的contentOffset

    CGFloat offsetY = self.scrollView.mj_offsetY;

    // 头部控件刚好全部出现的offsetY,默认是-64(20 + 44)

    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;

 // 向上滚动,直接返回

    if (offsetY > happenOffsetY) return;

    // 从普通 到 即将刷新 的临界距离

    CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;// -64 - 54 = -118

    //下拉的百分比:下拉的距离与header高度的比值

    CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;

    if (self.scrollView.isDragging) {

      //记录当前下拉的百分比

        self.pullingPercent = pullingPercent;

        if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {

            // 如果当前为默认状态 && 下拉的距离大于临界距离(将tableview下拉得很低),则将状态切换为可以刷新

            self.state = MJRefreshStatePulling;

       } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {

            // 如果当前状态为可以刷新 && 下拉的距离小于临界距离,则将状态切换为默认

            self.state = MJRefreshStateIdle;

        }

    } else if (self.state == MJRefreshStatePulling) {// 即将刷新 && 手松开

        // 手松开 && 状态为可以刷新(MJRefreshStatePulling)时 开始刷新

        [self beginRefreshing];

   } else if (pullingPercent < 1) {

        //手松开后,默认状态时,恢复self.pullingPercent

        self.pullingPercent = pullingPercent;       

    }

}

5.在切换状态时,执行相应的操作。实现方法是:setState:。

- (void)setState:(MJRefreshState)state

{

    MJRefreshCheckState


    if (state == MJRefreshStateIdle) {


        //============== 设置状态为默认状态 =============//


        //如果当前不是正在刷新就返回,因为这个方法主要针对从正在刷新状态(oldstate)到默认状态

        if (oldState != MJRefreshStateRefreshing) return;


        //刷新完成后,保存刷新完成的时间

        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];

        [[NSUserDefaults standardUserDefaults] synchronize];


        // 恢复inset和offset

        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{


            //118 -> 64(剪去了header的高度)

            self.scrollView.mj_insetT += self.insetTDelta;


            // 自动调整透明度

            if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;


        } completion:^(BOOL finished) {


            self.pullingPercent = 0.0;


            if (self.endRefreshingCompletionBlock) {

                //调用刷新完成的block

                self.endRefreshingCompletionBlock();

            }

        }];


    } else if (state == MJRefreshStateRefreshing) {


        //============== 设置状态为正在刷新状态 =============//

        dispatch_async(dispatch_get_main_queue(), ^{


            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{


                CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;//64 + 54 (都是默认的高度)

                // 重新设置contentInset,top = 118

                self.scrollView.mj_insetT = top;

                // 设置滚动位置

                [self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];


            } completion:^(BOOL finished) {

                //调用进行刷新的block

                [self executeRefreshingCallback];

            }];

        });

    }

}

MJRefreshStateHeader


这个类是MJRefreshHeader类的子类,它做了两件事:

1.简单布局了stateLabel和lastUpdatedTimeLabel。

2.根据控件状态的切换(默认状态,正在刷新状态),实现了这两个label显示的文字的切换。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容