MJRefresh源码分析 下拉刷新

MJRefresh是李明杰老师的一个开源项目,GitHub目前已经有10000多star,GitHub地址是MJRefresh
下面我们一起来分析下MJRefresh框架的实现过程。

  • MJRefresh中类与类之间的联系


    mjrefresh.png
  • 从我们使用MJRefresh框架的调用代码分析
    eg:
 self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
   // 属性中的回调
    }];
 [self.tableView.mj_header beginRefreshing];

上面的代码会调用MJRefreshNormalHeader父类MJRefreshStateHeader的父类MJRefreshHeader的方法:

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
  // 实例化MJRefreshHeader的对象
    MJRefreshHeader *cmp = [[self alloc] init];
    // refreshingBlock 父类的属性,把refreshingBlock赋值cmp.refreshingBlock属性
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}

上面的headerWithRefreshingBlock:refreshingBlock;方法实例化一个一个对象cmp,会触发MJRefreshHeader父类中的- (instancetype)initWithFrame:(CGRect)frame的方法。

#pragma mark - 初始化方法
- (instancetype)initWithFrame:(CGRect)frame
{
  // 注意,此时的self 是 MJRefreshNormalHeader的对象,为什么是 MJRefreshNormalHeader的对象,设计到继承的知识点,可以具体参考继承,这里就不过多的说明
    if (self = [super initWithFrame:frame]) {
        //  调用 MJRefreshNormalHeader 中prepare方法
        [self prepare];
         // 默认是普通状态,调用MJRefreshNormalHeadersetState方法
        self.state = MJRefreshStateIdle;
    }
    return self;
}

我们回到MJRefreshNormalHeader类中的prepare方法,方法具体实现如下

#pragma mark - 重写父类的方法
- (void)prepare
{
    // 调用父类的 prepare 父类 是 MJRefreshStateHeader
    [super prepare];
    // 设置菊花样式
    self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
}

此时又会去调用MJRefreshNormalHeader父类MJRefreshStateHeader中的prepare的方法

- (void)prepare
{
    [super prepare];
    // 初始化间距  文字距离圈圈、箭头的距离
    self.labelLeftInset = MJRefreshLabelLeftInset;
    // 初始化文字 国际化,中文,英文,繁体,
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing];
}

然后又会去调用父类中的prepare的方法,直到MJRefreshComponent类中的prepare的方法执行完毕。关于prepare方法,里面都是做一些初始化和frame的设置,比较简单,就不具体分析了。

再回到最开始的方法

self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
  // 属性中的回调
   }];

MJRefreshNormalHeader视图赋值给mj_headermj_headerUIScrollView+MJRefresh类中的属性,要给分类添加属性,就要用到runtime机制,具体代码如下:

#pragma mark - header
static const char MJRefreshHeaderKey = '\0';
- (void)setMj_header:(MJRefreshHeader *)mj_header
{
    if (mj_header != self.mj_header) {
        // 删除旧的,添加新的
        [self.mj_header removeFromSuperview];
        // A insertSubView B AtIndex:2 是将B插入到A的子视图index为2的位置(最底下是0)
        // eg [self addsuview: mj_header];
        [self insertSubview:mj_header atIndex:0];
        // 手动kvo
        [self willChangeValueForKey:@"mj_header"]; // KVO
        // 给分类中的属性添加一个set方法....
        // 分类能添加属性。但是不会自己生成getter和setter方法
        objc_setAssociatedObject(self, &MJRefreshHeaderKey,
                                 mj_header, OBJC_ASSOCIATION_ASSIGN);
        // 手动kvo
        [self didChangeValueForKey:@"mj_header"]; // KVO
    }
}
// get方法
- (MJRefreshHeader *)mj_header
{
    return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}

setMj_header的方法中,监听属性用了iOS 的设计模式 KVO

 [self willChangeValueForKey:@"mj_header"]; // KVO
 [self didChangeValueForKey:@"mj_header"]; // KVO

为什么要用 willChangeValueForKeydidChangeValueForKey方法监听分类的中的属性,而不是- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;具体可以参考KVO的在分类中的用法

[self insertSubview:mj_header atIndex:0]; A insertSubView B AtIndex:0是将B插入到A的子视图index为0的位置。

1、这句代码会触发MJRefreshComponent类中的- (void)willMoveToSuperview:(nullable UIView *)newSuperview;此方法什么时候被调用?经过查资料得知:当视图即将加入父视图时或者当视图即将从父视图移除时调用,具体我们分析下此方法

// newSuperview 就是父视图 这里值得 uiscrollerView
- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    // 如果不是UIScrollView,不做任何事情
    if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
    // 旧的父控件移除监听
    [self removeObservers];
    if (newSuperview) { // 新的父控件
        // 设置宽度
        self.mj_w = newSuperview.mj_w;
        // 设置位置
        self.mj_x = 0;
        // 记录UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // 设置永远支持垂直弹簧效果
        _scrollView.alwaysBounceVertical = YES;
        // 记录UIScrollView最开始的contentInset
        _scrollViewOriginalInset = _scrollView.contentInset;
       ;
        NSLog(@"contentInset:%@",NSStringFromUIEdgeInsets(_scrollView.contentInset));
        // 添加监听
        [self addObservers];
    }
}

此方法中的 self.mj_w = newSuperview.mj_w; self就是下拉的展示出来的viewmj_wUIView+MJExtension中的属性,实现的set的方法- (void)setMj_w:(CGFloat)mj_w,具体方法实现如下

- (void)setMj_w:(CGFloat)mj_w
{
    CGRect frame = self.frame;
    frame.size.width = mj_w;
    self.frame = frame;
}

分析到这里,应该明白了self.mj_w = newSuperview.mj_w;的意思了。self.mj_w = CGRectMake(original, original, newSuperview.mj_w, original);

[self addObservers];用KVO添加监听,给当前的UIScrollView添加了contentOffsetcontentSizepanGestureRecognizer 的监听

2、 [self insertSubview:mj_header atIndex:0];此方法还会触发MJRefreshComponent 类中layoutSubviews方法,触发 layoutSubviews 有哪些操作?
找了下资料并总结下:
1、调用 addSubview 方法时会执行该方法
2、设置并改变视图的frame属性时会触发该方法
3、滑动UIScrollView及继承与UIScrollView的控件时会触发该方法
4、旋转屏幕时,会触发父视图的layoutSubviews方法、设置并改变视图的frame属性时会触发父视图的layoutSubviews方法

OK,咱们一起看看MJRefreshComponent类中的layoutSubviews方法

- (void)layoutSubviews
{
// 此处的self依然是MJRefreshNormalHeader的对象
    [self placeSubviews];
    [super layoutSubviews];
}

MJRefreshNormalHeader类中的placeSubviews 添加了两个视图arrowView(箭头视图)、loadingView(菊花视图)
MJRefreshStateHeader类中的placeSubviews 添加了两个视图stateLabel(状态label )、lastUpdatedTimeLabel(显示时间label)
MJRefreshHeader类中的placeSubviews 添加了设置了当前视图的Y坐标
MJRefreshComponent类中的placeSubviews 没有干啥 ☺
鉴于placeSubviews方法比较简单,都是关于界面的搭建,再次就不多多啰嗦了。

OK,分析到这里界面啥的都出来了。下面具体分析下拉的视图如何出现
由于监听了UIScrollViewcontentOffset属性,当我们下拉的时候,触发监听方法。监听方法在MJRefreshHeader类中

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    
    // 在刷新的refreshing状态
    if (self.state == MJRefreshStateRefreshing) {
        if (self.window == nil) return;
        
        NSLog(@"%@",NSStringFromCGPoint(self.scrollView.contentOffset));
        // sectionheader停留解决
        
        //- self.scrollView.mj_offsetY:-(-54)= 54 : 刷新的时候,偏移量是不动的。偏移量 = 状态栏 + 导航栏 + header的高度
        //_scrollViewOriginalInset.top:64 (状态栏 + 导航栏)
        //insetT 取二者之间大的那一个
        CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
        
        insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
        self.scrollView.mj_insetT = insetT;
        
         // 记录刷新的时候的偏移量
        self.insetTDelta = _scrollViewOriginalInset.top - insetT;
        return;
    }
    NSLog(@"scrollViewContentOffsetDidChange");
    // 跳转到下一个控制器时,contentInset可能会变
     _scrollViewOriginalInset = self.scrollView.contentInset;
    // 当前的contentOffset  Y
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 头部控件刚好出现的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 如果是向上滚动到看不见头部控件,直接返回
    // >= -> >
    // 解释下: offsetY    正值 就是上滑动
    //         offsetY   负值  就是下拉
     if (offsetY > happenOffsetY) return;
    
    // 从普通 到 即将刷新 的临界距离 normal2pullingOffsetY = -54
    CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
    
    //下拉的百分比:下拉的距离与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) {// 即将刷新 && 手松开
        // 开始刷新
        [self beginRefreshing];
    }
    else if (pullingPercent < 1) {
                self.pullingPercent = pullingPercent;
    }
}

根据不同的state展示界面
MJRefreshStateHeader中的setState方法

- (void)setState:(MJRefreshState)state
{
//    MJRefreshCheckState
    // 状态检查
//#define MJRefreshCheckState \
    
    MJRefreshState oldState = self.state;
    if (state == oldState) return; 
    [super setState:state];
    // 设置状态文字
    self.stateLabel.text = self.stateTitles[@(state)];
    // 重新设置key(重新显示时间)
    self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}

MJRefreshNormalHeader中的setState方法

- (void)setState:(MJRefreshState)state
{

    MJRefreshState oldState = self.state;
    if (state == oldState) return;
    [super setState:state];
    
    // 根据状态做事情
    if (state == MJRefreshStateIdle) {
        
        if (oldState == MJRefreshStateRefreshing) {
            // 现在的状态是 MJRefreshStateIdle ,上一个状态时 MJRefreshStateRefreshing
            self.arrowView.transform = CGAffineTransformIdentity;
            
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                self.loadingView.alpha = 0.0;
            } completion:^(BOOL finished) {
                // 如果执行完动画发现不是idle状态,就直接返回,进入其他状态
                if (self.state != MJRefreshStateIdle) return;
                
                self.loadingView.alpha = 1.0;
                [self.loadingView stopAnimating];
                self.arrowView.hidden = NO;
            }];
        } else {
            // 当它停止的时候,菊花视图就会自动隐藏。
//             loadingView.hidesWhenStopped = YES;
            [self.loadingView stopAnimating];
            self.arrowView.hidden = NO;
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                self.arrowView.transform = CGAffineTransformIdentity;
            }];
        }
    } else if (state == MJRefreshStatePulling) {
        // loadingView 就是菊花的视图
        [self.loadingView stopAnimating];
        // 箭头视图
        self.arrowView.hidden = NO;
        // 让箭头旋转180°
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
            self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
        }];
        
    }
    
    else if (state == MJRefreshStateRefreshing) {
        self.loadingView.alpha = 1.0; // 防止refreshing -> idle的动画完毕动作没有被执行
        [self.loadingView startAnimating];
        self.arrowView.hidden = YES;
    }
}

MJRefreshStateHeader中的setState方法

- (void)setState:(MJRefreshState)state
{
//    MJRefreshCheckState
    // 状态检查
//#define MJRefreshCheckState \
    
    MJRefreshState oldState = self.state;
    if (state == oldState) return; 
    [super setState:state];
    // 设置状态文字
    self.stateLabel.text = self.stateTitles[@(state)];
    // 重新设置key(重新显示时间)
    self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}

MJRefreshNormalHeader中的setState方法

- (void)setState:(MJRefreshState)state
{
    // MJRefreshCheckState
    // 状态检查
    //#define MJRefreshCheckState \
    
    MJRefreshState oldState = self.state;
    if (state == oldState) return;
    [super setState:state];

    // 根据状态做事情
    if (state == MJRefreshStateIdle) {
        if (oldState != MJRefreshStateRefreshing) return;
        // 当前的状态必须是 MJRefreshStateIdle ,上一个状态是 MJRefreshStateRefreshing,才可以保存时间和恢复uiscrollerView的 inset和 offset
        
        // 保存刷新时间
        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        NSLog(@"MJRefreshState");
        // 恢复inset和offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            self.scrollView.mj_insetT += self.insetTDelta;
            NSLog(@"%@",NSStringFromUIEdgeInsets(self.scrollView.contentInset));
            // 自动调整透明度
            if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
        } completion:^(BOOL finished) {
            self.pullingPercent = 0.0;
              if (self.endRefreshingCompletionBlock) {
                self.endRefreshingCompletionBlock();
            }
        }];
    }
    else if (state == MJRefreshStateRefreshing) {
        // 对UI的调度,都应该在主线程中
         dispatch_async(dispatch_get_main_queue(), ^{
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
                // 增加滚动区域top
                self.scrollView.mj_insetT = top;
                // 设置滚动位置
                [self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
            } completion:^(BOOL finished) {
                [self executeRefreshingCallback];
            }];
         });
    }
}

MJRefreshComponent中的setState方法

- (void)setState:(MJRefreshState)state
{
    _state = state;
    // 加入主队列的目的是等setState:方法调用完毕、设置完文字后再去布局子控件
    dispatch_async(dispatch_get_main_queue(), ^{
        [self setNeedsLayout];
    });
}

关于下拉刷新,分析就到此为止,更多用法,参考MJRefreshDemo

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

推荐阅读更多精彩内容