分析MJRefresh框架,并模拟上拉加载更多

先说下下拉刷新动画效果的实现,重写写一个动画的类,并且重写prepare方法,在这里面添加UI,并在placeSubviews方法中设置她的frame和坐标,因为placeSubviews方法是写在layoutSubviews方法里面的。

- (void)prepare {
    
    [super prepare];
    [self addSubview:self.gifImageView];
    
}

- (void)placeSubviews {
    
    [super placeSubviews];
    CGFloat stateTextWidth = self.stateLabel.textWidth;
    CGFloat lastTimeTextWidth = self.lastUpdatedTimeLable.textWidth;
    CGFloat finalTextWidth = MAX(stateTextWidth, lastTimeTextWidth);
    _gifImageView.center = CGPointMake((self.eoc_w - finalTextWidth)/4, self.eoc_h/2-20.f);
    _gifImageView.image = [_stateImages[@(EOCRefreshStateIdle)] firstObject];
    _gifImageView.eoc_size = _gifImageView.image.size;
    
}

其次动画是一帧一帧的,我们根据下拉的比例来决定显示哪张照片,因为GIF动画其实是一组照片依次显示出来的。

- (void)setPullingPercent:(CGFloat)pullingPercent {
    
    [super setPullingPercent:pullingPercent];
    NSArray *images = self.stateImages[@(EOCRefreshStateIdle)];
    if (self.state != EOCRefreshStateIdle || images.count == 0) return;
    // 停止动画
    [self.gifImageView stopAnimating];
    // 设置当前需要显示的图片
    NSUInteger index =  images.count * pullingPercent;
    if (index >= images.count) index = images.count - 1;
    self.gifImageView.image = images[index];
    
}

最后在刷新和下拉状态的时候开始动画,在闲置状态的时候结束动画。

- (void)setState:(EOCRefreshState)state {
    [super setState:state];
    if (state == EOCRefreshStateRefreshing || state == EOCRefreshStatePulling) {
        _gifImageView.animationImages = _stateImages[@(EOCRefreshStateRefreshing)];
        _gifImageView.animationDuration = [_stateAnimationDurations[@(EOCRefreshStateRefreshing)] doubleValue];
        [_gifImageView startAnimating];
    } else if (state == EOCRefreshStateIdle) {
        [_gifImageView stopAnimating];
    }
}

上拉加载更多会涉及到ContentSize和GestureState的变化,所以基类里面增加了

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

这两个公开的方法,并且通过KVO进行监听。

- (void)willMoveToSuperview:(UIView *)newSuperview {
    //当self被添加到superView的时候,调用
    if (newSuperview && [newSuperview isKindOfClass:[UIScrollView class]]) {
        
        //非空,而且是UIScrollView
        //同一个header被不同的table来添加的时候

       //这里的 self.superView 对应的ATableView
    if (self.superview && [self.superview isKindOfClass:[UIScrollView class]]) {
        
        UIScrollView *lastSuperView = (UIScrollView *)self.superview;
        [lastSuperView removeObserver:self forKeyPath:@"contentOffset"];
        [lastSuperView removeObserver:self forKeyPath:@"contentSize"];
        [lastSuperView.panGestureRecognizer removeObserver:self forKeyPath:@"state"];
    }
        
        self.scrollView = (UIScrollView *)newSuperview;
        self.originalScrollInsets = self.scrollView.contentInset;
        //控件还没有设置frame
        self.eoc_x = 0.f;
        self.eoc_w = self.scrollView.eoc_w;
        
        //footer和header都继承,这两者的高度是不一样
    
        [_scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
        
        [_scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
        
        [_scrollView.panGestureRecognizer addObserver:self forKeyPath:@"state" options:NSKeyValueObservingOptionNew context:nil];
    }
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    if ([keyPath isEqualToString:@"contentOffset"]) {
        [self scrollOffsetDidChange:change];
    } else if ([keyPath isEqualToString:@"contentSize"]) {
        [self scrollViewContentSizeDidChange:change];
    } else if ([keyPath isEqualToString:@"state"]) {
        [self scrollViewGestureStateDidChange:change];
    }
}

AutoFooter

一直都会,刚开始就会出现在tableView的底部

内容超过了一屏scrollView的大小的时候

这里的一屏并不一定是屏幕大小,而是scrollView的frame大小。这个时候,当yOffset大于内容高度减去scrollView本身高度后,加上footer的高度的和就完全显示出footer。


其中红色为scrollView的frame,蓝色为内容的大小,橙色为footer的大小,当滑动的距离超过下面红色那段的时候就完全显示出footer了。

- (void)scrollOffsetDidChange:(NSDictionary *)change {
    //如果内容超过了一屏scrollView的大小
    if (self.scrollView.eoc_h < self.scrollView.eoc_contentH + self.scrollView.eoc_insetT) {
        if (self.scrollView.contentOffset.y >= self.scrollView.eoc_contentH - self.scrollView.eoc_h + self.eoc_h) {
            //完全显示出footer
            // 防止手松开时连续调用
            CGPoint old = [change[@"old"] CGPointValue];
            CGPoint new = [change[@"new"] CGPointValue];
            if (new.y <= old.y) return;  // 新的Y小于旧的Y说明,往上拉动的距离不够,或者是footer向下离开屏幕的过程,这个时候直接返回,不进行刷新
            self.state = EOCRefreshStateRefreshing;
        }
    }
}
注意1

设置scrollView的contentInset底部为footer的高,即增加可视范围,完全显示出AutoFooter

- (void)willMoveToSuperview:(UIView *)newSuperview {
    
    [super willMoveToSuperview:newSuperview];
    
    if (newSuperview) {
        //设置scrollView的contentInset底部为footer的高,即增加了滑动距离即可视范围刚刚好为footer的高,完全显示出AutoFooter
        self.scrollView.eoc_insetB = self.eoc_h;
        self.eoc_y = self.scrollView.eoc_contentH;
    } else {  //self被移除掉
        //修改还原scrollView的contentInset
        self.scrollView.eoc_insetB = self.originalScrollInsets.bottom;
    }
}
注意2

footer的Y坐标需要专门在监听contentSize的方法中设置,因为只有有了contentSize的时候,才能设置在contentSize的底部,不然当contentSize为零的时候,就加载在tableView的头部去了

- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
    //contentSize发生变化,一般是tableView发生变化
    self.eoc_y = self.scrollView.eoc_contentH;
}
内容没有超过一屏scrollView的大小的时候

这个时候tableView是无法滚动的,需要来监听手势,通过公开的手势监听方法来实现

- (void)scrollViewGestureStateDidChange:(NSDictionary *)change {
    //如果在一屏的时候
    if (self.scrollView.eoc_h > self.scrollView.eoc_contentH + self.scrollView.eoc_insetT) {  // 内容小于一个屏幕时
         CGPoint transitionPoint = [self.scrollView.panGestureRecognizer translationInView:self.scrollView];
        if (transitionPoint.y < 0 && self.scrollView.panGestureRecognizer.state == UIGestureRecognizerStateEnded)
        {
          //往上拉,手势不能动
            self.state = EOCRefreshStateRefreshing;
        }
    } else {  //超过一屏的时候
        if (self.scrollView.eoc_offsetY >= self.scrollView.eoc_contentH + self.scrollView.eoc_insetB - self.scrollView.eoc_h ) {
            self.state = EOCRefreshStateRefreshing;
        }
    }
}

BackFooter

需要向上拖动一定的距离才会显现,并且刷新的时候停留在底部,当加载更多完了过后,就消失。

注意1

其中Y坐标的设定分为两种情况,一种是内容超过了scrollView的frame,Y坐标应该紧跟着内容的后面,另一种是内容没有超过scrollView的frame,Y坐标应该紧跟在scrollView的后面,如果有InsetTop值还要考虑减去它,因为它的坐标是从InsetTop下面才开始计算的,不然Y坐标会向下移动top的距离。

- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
    //内容和contentSize进行比对
    CGFloat contentSizeH = self.scrollView.eoc_contentH;
    //这里必须是_originalEdgeInsets
    CGFloat contentHeight = self.scrollView.eoc_h - self.originalScrollInsets.top - self.originalScrollInsets.bottom;  //减去eoc_insetT,是希望在上拉scrollView的时候就显示出来,减去eoc_insetB,是把eoc_footer变成内容区域块
    self.eoc_y = MAX(contentSizeH, contentHeight);
}

上面红色为手机屏幕大小,紫色为scrollView的大小,灰色为内容的大小,青色为footer的大小,灰色和紫色之间为top值。

注意2

找临界点,即刚刚出现footer头部时候的值
分为两种情况:

  • 内容超过scrollView的frame,即contentSize的H大于scrollView的H
    临界值就等于contentSize的H减去scrollView的H
  • 内容小于scrollView的frame的时候,即为inset的Top值
- (CGFloat)boundaryOffset {
    
    //内容和contentSize进行比对
    CGFloat contentSizeH = self.scrollView.eoc_contentH;
    CGFloat contentHeight = self.scrollView.eoc_h - self.scrollView.eoc_insetT - self.scrollView.eoc_insetB;  //减去eoc_insetT,是希望在上拉scrollView的时候就显示出来,减去eoc_insetB,是把eoc_footer变成内容区域块
    CGFloat finalY = MAX(contentSizeH, contentHeight);
    if (finalY == contentSizeH) {
//        return _scrollView.eoc_contentH - _scrollView.eoc_h + _scrollView.eoc_insetB;
        return contentSizeH - self.scrollView.eoc_h;
    } else {
        return -self.scrollView.eoc_insetT;
    }
}
注意3

让其在刷新的时候,让footer保持显示,刷新完成就消失

  • 内容小于scrollView的frame的时候

如上图,原来的展示范围只是到灰色框为止,而现在要是footer展示出来,所以要将展示的距离增加蓝色的高度再加上footer的高度,如果原来还有bottom,还有加上原来的bottom,并且将这个新加的和设置为新的Inset的bottom的值,这样footer就能够展示了。

  • 内容大于scrollView的frame的时候
    要使footer完全显示出来,要将Inset的bottom的值设置为footer的高度,如果有原来的bottom,还要加上原来的bottom值。
    并且还要使tableView滚动到最底部,这样才能看到footer,即要将Offset设置为原来算出来内容大于scrollView的frame时候的临界值,即刚刚露出footer头部,再加上footer的高度和新设置的Inset的bottom的值。

-(void)setState:(EOCRefreshState)state {
    [super setState:state];
    if (state == EOCRefreshStateRefreshing) {
        [UIView animateWithDuration:0.25f animations:^{
            CGFloat bottom = self.eoc_h + self.originalScrollInsets.bottom;
            CGFloat contentSizeH = self.scrollView.eoc_contentH;
            CGFloat contentHeight = self.scrollView.eoc_h - self.originalScrollInsets.top - self.originalScrollInsets.bottom;
            CGFloat deltaH = contentSizeH - contentHeight;
            
            if (deltaH < 0) { // 如果内容高度小于view的高度
                bottom -= deltaH;  // 因为deltaH < 0,所以bottom -= deltaH 相当于加上了一个绝对值为deltaH的正值,即可视范围增加了deltaH的距离
            }
            self.scrollView.eoc_insetB = bottom;
            
            self.scrollView.eoc_offsetY = [self boundaryOffset] + self.eoc_h + self.scrollView.eoc_insetB;
        } completion:^(BOOL finished) {
             [self beginRefresh];
        }];
    } else if (state == EOCRefreshStateIdle || state == EOCRefreshStateNoMoreData) {
        [UIView animateWithDuration:0.25f animations:^{
            // 刷新完了过后,回到初始值,即隐藏掉footer
            self.scrollView.eoc_insetB = self.originalScrollInsets.bottom;
        } completion:^(BOOL finished) {
        }];
    }
}

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

推荐阅读更多精彩内容