美文网首页iOS学习iOS初学者iOS Developer
iOS仿微信朋友圈下拉刷新

iOS仿微信朋友圈下拉刷新

作者: 上北以北 | 来源:发表于2017-06-02 15:40 被阅读320次

    一个仿微信朋友圈的下拉刷新

    使用方法 点击demo链接查看ReadMe
    demo链接: https://github.com/Xiexingda/XDRefresh.git
    喜欢的话请在github给颗小星星哦😊!

    基本思路:

    用一个与下拉刷新小圆圈一样大小的scrollview,把其contentSize也置为同样大小,然后把下拉刷新的小圆圈放到scrollview上,这样在下拉刷新过程中只需要根据被观察者的下拉状态去改变这个scrollview的contentoffset.y即可实现小圆圈的上下移动,而不需要去渲染下拉小圆圈的frame

    实现过程:

    刷新过程主要分为三种状态

    typedef NS_ENUM(NSInteger,StatusOfRefresh) {
        XDREFRESH_Default = 1,     //非刷新状态,该值不能为0
        XDREFRESH_BeginRefresh,    //刷新状态
        XDREFRESH_None             //全非状态(即不是刷新 也不是 非刷新状态)
    };
    
    @property (nonatomic, assign)CGFloat threshold;//下拉位置的最大范围
    

    主要方法,通过kvo去观察tableview的下拉过程

    /**
     添加观察者
    
     @param view 观察对象
     */
    - (void)addObserverForView:(UIView *)view {
        [view addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    }
    

    实现观察者的代理 并在其中实现三种状态(非刷新,刷新,(全非)既不刷新也不非刷新)

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        
        //非状态时屏蔽掉其他的操作
        if (self.refreshStatus == XDREFRESH_None) {
            return;
        }
        
        //屏蔽掉开始进入界面时的系统下拉动作
        if (self.refreshStatus == 0) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                self.refreshStatus = XDREFRESH_Default;
            });
            return;
        }
        
        // 实时监测scrollView.contentInset.top, 系统优化以及手动设置contentInset都会影响contentInset.top。
        if (self.marginTop != self.extenScrollView.contentInset.top) {
            self.marginTop = self.extenScrollView.contentInset.top;
        }
        
        CGFloat offsetY = self.extenScrollView.contentOffset.y;
        
        /**异步调用主线程**/
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_async(dispatch_get_main_queue(), ^{
                /**非刷新状态**/
                if (self.refreshStatus == XDREFRESH_Default) {
                    [self defaultHandleWithOffSet:offsetY change:change];
                    
                    /**刷新状态**/
                } else if (self.refreshStatus == XDREFRESH_BeginRefresh) {
                    [self refreshingHandleWithOffSet:offsetY];
                }
            });
        });
    }
    

    全非状态时直接return 以屏蔽掉刷新、非刷新状态 (刷新小圆圈在下拉悬停状态时进入全非状态,待刷新完成后自动收回,这个过程应避免人为干预造成卡顿,而刷新、和非刷新状态人为拉动时都会干预到小圆圈的contentoffset所以要屏蔽掉)

    //非状态时屏蔽掉其他的操作
        if (self.refreshStatus == XDREFRESH_None) {
            return;
        }
    

    非刷新状态逻辑

    /**
     非刷新状态时的处理
    
     @param offsetY tableview滚动偏移量
     */
    - (void)defaultHandleWithOffSet:(CGFloat)offsetY change:(NSDictionary<NSKeyValueChangeKey,id> *)change {
        // 向下滑动时<0,向上滑动时>0;
        CGFloat defaultoffsetY = offsetY + self.marginTop;
        
        /**刷新动作区间**/
        if (defaultoffsetY > self.threshold && defaultoffsetY < 0) {
            [self.refreshView setContentOffset:CGPointMake(0, defaultoffsetY)];
            
                            /*
                             注意:将default动作处理只放到 动作区间 和 超过/等于 临界点 的逻辑块里
                             目的:实现只有在下拉动作时才会有动作处理,否则没有
                             
                             */
                            [self anmiationHandelwithChange:change
                                                  andStatus:XDREFRESH_Default
                                              needAnimation:YES];
        }
        
        /**(@"刷新临界点,把刷新icon置为最大区间")**/
        if (defaultoffsetY <= self.threshold && self.refreshView.contentOffset.y != self.threshold) {
            //添加动作,避免越级过大造成直接跳到最大位置影响体验
            [UIView animateWithDuration:0.05 animations:^{
                [self.refreshView setContentOffset:CGPointMake(0, self.threshold)];
            }];
        }
        
        /**超过/等于 临界点后松手开始刷新,不松手则不刷新**/
        if (defaultoffsetY <= self.threshold && self.refreshView.contentOffset.y == self.threshold) {
            if (self.extenScrollView.isDragging) {
                                //NSLog(@"不刷新");
                                //default动作处理
                                [self anmiationHandelwithChange:change
                                                      andStatus:XDREFRESH_Default
                                                  needAnimation:YES];
                
            } else {
                                //NSLog(@"开始刷新");
                                //刷新状态动作处理
                                [self anmiationHandelwithChange:change
                                                      andStatus:XDREFRESH_BeginRefresh
                                                  needAnimation:YES];
                                // 由非刷新状态 进入 刷新状态
                                [self beginRefresh];
            }
        }
        
        /**当tableview回滚到顶端的时候把刷新的iconPosition置零**/
        if (defaultoffsetY >= 0 && self.refreshView.contentOffset.y != 0) {
            [self.refreshView setContentOffset:CGPointMake(0, 0)];
            //当回到原始位置后,转角也回到原始位置
            [self trangleToBeOriginal];
        }
    }
    

    刷新状态逻辑

    /**
     刷新状态时的处理
    
     @param offsetY tableview滚动偏移量
     */
    - (void)refreshingHandleWithOffSet:(CGFloat)offsetY {
        //转换坐标(相对费刷新状态)
        CGFloat refreshoffsetY = offsetY + self.marginTop + self.threshold;
        /**刷新状态时动作区间**/
        if (refreshoffsetY > self.threshold && refreshoffsetY < 0) {
            [self.refreshView setContentOffset:CGPointMake(0, refreshoffsetY)];
        }
        
        /**刷新状态临界点,把刷新icon置为最大区间**/
        if (refreshoffsetY <= self.threshold && self.refreshView.contentOffset.y != self.threshold) {
            //添加动作,避免越级过大造成直接跳到最大位置影响体验
            [UIView animateWithDuration:0.05 animations:^{
                [self.refreshView setContentOffset:CGPointMake(0, self.threshold)];
            }];
        }
        
        /**当tableview相对坐标回滚到顶端的时候把刷新的iconPosition置零**/
        if (refreshoffsetY >= 0 && self.refreshView.contentOffset.y != 0) {
            [self.refreshView setContentOffset:CGPointMake(0, 0)];
        }
    }
    

    刷新

    /**
     开始刷新
     */
    - (void)beginRefresh {
        //状态取反 保证一次刷新只执行一次回调
        if (self.refreshStatus != XDREFRESH_BeginRefresh) {
            self.refreshStatus = XDREFRESH_BeginRefresh;
            if (self.refreshBlock) {
                self.refreshBlock();
            }
        }
    }
    

    动画效果

    /**
     动作处理
    
     @param change 监听到的offset变化
     */
    - (void)anmiationHandelwithChange:(NSDictionary<NSKeyValueChangeKey,id> *)change andStatus:(StatusOfRefresh)status needAnimation:(BOOL)need {
        if (!need) {
            return;
        }
        
        /**
         非刷新状态下的动作处理
         */
        if (status == XDREFRESH_Default) {
            /**把nsPoint结构体转换为cgPoint**/
            CGPoint oldPoint;
                    id oldValue = [change valueForKey:NSKeyValueChangeOldKey];
                    [(NSValue*)oldValue getValue:&oldPoint];
            
            
            CGPoint newPoint;
                    id newValue = [ change valueForKey:NSKeyValueChangeNewKey ];
                    [(NSValue*)newValue getValue:&newPoint];
            
            dispatch_async(dispatch_get_main_queue(), ^{
                if (oldPoint.y < newPoint.y) {
                    self.refreshView.refreshIcon.transform = CGAffineTransformRotate(self.refreshView.refreshIcon.transform,
                                                                                   -self.offsetCollect/50);
                    
                    NSLog(@"向上拉动");
                } else if (oldPoint.y > newPoint.y) {
                    self.refreshView.refreshIcon.transform = CGAffineTransformRotate(self.refreshView.refreshIcon.transform,
                                                                                   self.offsetCollect/50);
                    
                    NSLog(@"向下拉动");
                    
                } else {
                    NSLog(@"没有拉动");
                }
            });
            
            /**
             刷新状态下的动作处理
             */
        } else if (status == XDREFRESH_BeginRefresh) {
            if (!self.animation) {
                self.animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
            }
            
                                dispatch_async(dispatch_get_main_queue(), ^{
                                    //逆时针效果
                                    self.animation.fromValue = [NSNumber numberWithFloat:0.f];
                                    self.animation.toValue =  [NSNumber numberWithFloat: -M_PI *2];
                                    self.animation.duration  = CircleTime;
                                    self.animation.autoreverses = NO;
                                    self.animation.fillMode =kCAFillModeForwards;
                                    self.animation.repeatCount = MAXFLOAT; //一直自旋转
                                    [self.refreshView.refreshIcon.layer addAnimation:self.animation forKey:@"refreshing"];
                                });
        }
    }
    

    动画结束后回到最初角度

    /**
     角度还原:用于非刷新时回到顶部 和 刷新状态endRefresh 中
     */
    - (void)trangleToBeOriginal {
        self.refreshView.refreshIcon.transform = self.refreshView.transform;
    }
    

    结束刷新

    - (void)endRefresh {
        /**
         仿微信当下拉一直拖住时,icon不会返回
         虽然在repeat的计时器里,但是该方法只会回调一次
         原理:nstimer默认是放在defaultrunloop中的,当下拉拖住时runloop改成了tracking模式,同一时间下线程只能处理一种runloop模式,所以滚动时timer只注册不执行,当松开手时拖拽动作执行完毕,runloop回到default模式下,这个时候nstimer被执,block开始回调,在第一次回调后又调用了invalidate方法将计时器释放了
         注意** 最后用invalidate把计时器释放掉
         */
        if (self.extenScrollView.isDragging) {
            //iOS10 以上
            if ([UIDevice currentDevice].systemVersion.floatValue >= 10) {
                [NSTimer scheduledTimerWithTimeInterval:0.2 repeats:YES block:^(NSTimer * _Nonnull timer) {
                    [self endRefresh];
                    [timer invalidate];
                }];
                
                //iOS10 以下
            } else {
                [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(timerCall:) userInfo:nil repeats:YES];
            }
            
            return;
        }
        
        //当结束刷新时,把状态置为全非状态,避免在[UIView animateWithDuration:0.2]icon返回动作中的人为干预,造成icon闪顿现象
        if (self.refreshStatus != XDREFRESH_None) {
            self.refreshStatus = XDREFRESH_None;
            
            [UIView animateWithDuration:IconBackTime animations:^{
                [self.refreshView setContentOffset:CGPointMake(0, 0)];
                
            } completion:^(BOOL finished) {
                //结束动画
                [self.refreshView.refreshIcon.layer removeAnimationForKey:@"refreshing"];
                
                //当回到原始位置后,转角也回到原始位置
                [self trangleToBeOriginal];
                
                //结束后将状态重置为非刷新状态 以备下次刷新
                self.refreshStatus = XDREFRESH_Default;
            }];
        }
    }
    
    /**
     计时器调用方法
    
     @param timer nstimer
     */
    - (void)timerCall:(NSTimer *)timer {
        [self endRefresh];
        [timer invalidate];
    }
    

    到此基本刷新逻辑已经完成了 ,还有一些结束刷新时的操作就不在这里赘述了,demo里面有详细的解析,有什么不合理的地方还望大家指出。
    demo链接: https://github.com/Xiexingda/XDRefresh.git
    使用方法在 该链接的ReadMe里
    喜欢的话请在github给颗小星星哦😊!

    效果:

    iTools-Screen-Record-2017-06-03-15.44.gif

    相关文章

      网友评论

      • ChanJaWe:你好,我用uicollectionview添加你的头部刷新,下拉崩溃了,id oldValue = [change valueForKey:NSKeyValueChangeOldKey]; 能麻烦你帮忙看一下吗?
        ChanJaWe:你好,打了断点,是崩在那句代码上了
        正常打印change是dictionary
        {
        kind = 1;
        new = "NSPoint: {0, 0}";
        old = "NSPoint: {0, 0}";
        }

        崩溃打印是这样的
        Printing description of change:
        (NSKeyValueChangeDictionary *) change = 0x0000604000878280
        上北以北:我给你留个微信私聊吧xxd779656694,你给我发的这个就是一个结构体的转换,这个一般不会有问题的
        上北以北:打一下断点,看看崩到哪了
      • 上北以北:刚从微博搬家过来,写的不是太规范,有疏漏的地方还望大家指正!
        Pusswzy:厉害

      本文标题:iOS仿微信朋友圈下拉刷新

      本文链接:https://www.haomeiwen.com/subject/dbkufxtx.html