iOS 惯性滑动效果

作者: _清墨 | 来源:发表于2017-07-27 17:40 被阅读1655次

    最近公司SDK新搞了个功能,手势滑动地图后,要具备惯性滑动效果的功能。安卓是先做出来了,然后给我看,由于我早体验过某鸟地图,某鸟地图也有这种效果,加上安卓做得确实不错,还在忙着研究OpenGL的我也只能先放下手中活,看着新功能默默构思了。

    先把结果放出来:

    寅时室内地图.gif

    讲一下写这篇文章的原因:安卓是由于有系统的api,在滑动手势结束后调用系统自有api,传入手势结束时的速度(x方向和y方向)就能由系统自己做完往后的操作。而iOS并没有,但我还是自以为这个功能很好做...然而构思之后发现还得找百度啊,但百度给我的结果却没有一个能满足我。所以,在我做出这个效果之后,我得将它分享出来,给有需要的人提供思路,也希望能相互讨论,接受到更好的办法做出更好的效果。(这就跟UIScrollView的滑动效果类似,但是网上是没有代码资料的)

    为了公司利益考虑,文章代码我专门写了demo来演示。

    进入正题:
    1.明确我们的目的:手势滑动后拥有惯性滑动效果
    2.思考具体实现:手滑得越快,作用对象的惯性越大,运动时间越长,手滑得慢,作用对象的运动速度就越小,运动时间也越短
    3.出现的一些小问题:解决它

    OK,想到第2点就已经可以成为嘴强王者了,接下来就看操作是不是青铜了:

    demo效果如下:

    自写demo.gif

    请大家不要看gif图好像有点卡,实际是一点都不卡的,很流畅很自然!


    动.gif

    demo中使用了两种方法让其做惯性滑动。

    一、第一种是在手势结束后通过UIView的动画来改蓝色图片的center,因为系统UIView的动画有快进慢出UIViewAnimationOptionCurveEaseOut这种效果可选。

    -(void)paned:(UIPanGestureRecognizer *)pan
    {
            CGPoint locationPoint = [pan locationInView:self.view];  //手指点
        
            CGPoint transPoint = [pan translationInView:self.view];  //移动点
        
            //    if (CGRectContainsPoint(blueImgView.frame, locationPoint)) {//若要只作用于蓝地图,以下代码移到此处
            
            //}
            
            blueImgView.center = CGPointMake(blueImgView.center.x+transPoint.x, blueImgView.center.y+transPoint.y);
            [pan setTranslation:CGPointZero inView:self.view];
            
            if (pan.state == UIGestureRecognizerStateEnded) {
                
                CGPoint velocity = [pan velocityInView:self.view];   //手指离开时x和y方向速度,单位是points/second
                CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y)); //真实速度
                CGFloat slideMult = magnitude / 200;  //自己试出来的比例,改动此处可修改灵敏度
                
                float slideFactor = 0.1 * slideMult;
                CGPoint finalPoint = CGPointMake(pan.view.center.x + (velocity.x * slideFactor),
                                                 pan.view.center.y + (velocity.y * slideFactor));
                finalPoint.x = MIN(MAX(finalPoint.x, 0), self.view.bounds.size.width);
                finalPoint.y = MIN(MAX(finalPoint.y, 0), self.view.bounds.size.height);
                
                [UIView animateWithDuration:slideFactor delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ //slideFactor秒内做完改变center的动画,动画效果快进慢出(先快后慢)
                    blueImgView.center = finalPoint;
                } completion:nil];
                
            }
     
    }
    

    重点是看UIGestureRecognizerStateEnded里的处理

    CGPoint velocity = [pan velocityInView:self.view];这个方法可以获取手势离开时在x,y方向的速度,单位是点每秒(逻辑尺寸点)。

    接着就是根据x、y的速度求出总速度,大家可以输出下velocity,看看它的数据,找到它的规律(我就是这样多次看,看出来的)。根据我们手滑动的快慢,velocity值也会跟着变化,总速度magnitude也会跟着变化,当然是手滑越快magnitude越大,越慢magnitude越小,那么,时间就用magnitude来确定吧,然后就试出来了除以200。另外我们根据velocity知道它在x,y方向上的速度,确定了运动时间,当然也能知道这段时间内它移动的距离:即 距离 = 速度 * 时间。 (毕竟读过小学)

    然后就是做UIView的动画了。

    [UIView animateWithDuration:slideFactor delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ //slideFactor秒内做完改变center的动画,动画效果快进慢出(先快后慢)
                    blueImgView.center = finalPoint;
                } completion:nil];
    

    第一种方法点评:个人觉得不太自然,可能系统UIViewAnimationOptionCurveEaseOut效果并不是很明显吧,当然也很有可能改改代码,调一调灵敏度,效果会好很多。 最重要的是:我们公司的产品用这种UIView的方式是实现不了的,使用的是矩阵transform,所以接下来就开始第二种方法:

    二、两种方法的区别在于处理手势滑动事件,第二种方法我们先定义了几个变量对象:

    @interface OtherViewController ()
    {
    
        UIImageView *blueImgView;
        
        CGAffineTransform viewTransform;   //基础self.view的transform
        CGAffineTransform currentTransform; //当前transform
        
        CADisplayLink *dis; //定时器
        int updateCount;    //需要刷新次数
        int currentCount;   //当前刷新次数
        CGPoint velocity;   //速度
    }
    

    然后在viewDidLoad里将 viewTransform = self.view.transform;

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        
        blueImgView = [[UIImageView alloc]init];
        blueImgView.frame = CGRectMake(50, 100, 100, 100);
        blueImgView.image = [UIImage imageNamed:@"地图1"];
        [self.view addSubview:blueImgView];
        
        UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(paned:)];
        [self.view addGestureRecognizer:pan];
        
        viewTransform = self.view.transform;
        
        UIButton *Btn = [UIButton buttonWithType:UIButtonTypeCustom];
        Btn.frame = CGRectMake(40, 40, 100, 40);
        Btn.backgroundColor = [UIColor grayColor];
        [Btn setTitle:@"上个界面" forState:UIControlStateNormal];
        [Btn addTarget:self action:@selector(Btn) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:Btn];
    }
    

    在手势滑动事件里我们使用到了CADisplayLink,CADisplayLink也是一种定时器,调用时间间隔跟屏幕刷新频率是一致的(1s60次,X出来了,好像是每秒120帧),为了使我们动画效果高效流畅,我们使用这个。

    -(void)paned:(UIPanGestureRecognizer *)pan
    {
        if (dis) {
            [dis invalidate];
            dis = nil;
        }
    
        CGPoint locationPoint = [pan locationInView:self.view];  //手指点
        
        CGPoint transPoint = [pan translationInView:self.view];  //移动点
        
        //    if (CGRectContainsPoint(blueImgView.frame, locationPoint)) {//若要只作用于蓝地图,以下代码移到此处
        
        //}
    
        currentTransform = CGAffineTransformTranslate(viewTransform, transPoint.x, transPoint.y);
        blueImgView.transform = currentTransform;
        
        if (pan.state == UIGestureRecognizerStateEnded) {
            viewTransform = currentTransform;
            
            velocity = [pan velocityInView:self.view];
            CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y));
            CGFloat slideMult = magnitude / 200;
            float slideFactor = 0.1 * slideMult;
            
            updateCount = slideFactor * 120 + 1;
            currentCount = 0;
            dis = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateView)];
            [dis addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        }
    
    }
    

    代码中关于速度的处理跟第一种方式一样,但接下来的动作是确定动画调用次数updateCount,为什么updateCount = slideFactor * 120 + 1;也是试出来的,本来是*60,大家可以自行更改看看效果。

    在CADisplayLink调用的方法里:

    -(void)updateView
    {
        
        currentCount++;
        if (currentCount>updateCount || currentCount>60) {
            
            //        dis.paused = YES;
            [dis removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
            [dis invalidate];
            dis = nil;
        }else{
            CGPoint point = CGPointMake(velocity.x/30.0/currentCount, velocity.y/30.0/currentCount);
            currentTransform = CGAffineTransformTranslate(viewTransform, point.x, point.y);
            blueImgView.transform = currentTransform;
            viewTransform = currentTransform;
        }
        
    }
    

    我们规定调用次数要不多于60次,即作用对象最多运动1s,在作用对象运动过程中

    CGPoint point = CGPointMake(velocity.x/30.0/currentCount, velocity.y/30.0/currentCount);
    

    point就是来确定后续运动时x,y方向速度的,velocity是x,y方向的速度,除以30可以得到一个运动较适合的速度值,除以currentCount的原因是让作用对象做减速运动,currentCount在递增,除以currentCount的话,运动速度就是递减了。 (方法完,可自行修改这个速度来改变灵敏度)

    总结:所有代码都在上面了,就不往github上放了。要是有帮到大家是我的荣幸,另外夏天热,可以帮我买块西瓜去去暑 %>_<%。

    相关文章

      网友评论

      • Calvin_Shen:如果要实现scrollView的惯性滑动效果,速度如何衰变?
        _清墨:@Calvin_Shen 那种衰减效果的算法,不告诉我们自然不可能完全一样,但自己可以模拟,可以这样,以手势离开的速度做匀减速(或是自己要求)运动,时间就随初速度而变了;也可以以固定时间来算,这样就有不同的衰减速度,这两种方案看你需求哪种更好
        Calvin_Shen:@_清墨 我知道这个demo是减速运动,但是速度衰减的有点生硬,如果想得到向UIScrollView那种的手指离开屏幕产生的惯性衰减效果,有方法么?
        _清墨:currentCount一直在递增,手离开后的惯性滑动自然是在减速运动
      • X1aoHey:作者大大,我想请教下,第一张gif里面那些不规则的图形是怎么布局的?一个色块是一个View吗?
        _清墨:都是layer,shapeLayer,对于大图,图形数量过多的需要做很大的优化计算才能让性能最优,你要也做这方面的东西,最好还是研究下openGL,并且3D也是绕不开的
      • _老花眼:不错我竟然都看懂了
        _清墨:可以,是不是我的陈述有些难懂?

      本文标题:iOS 惯性滑动效果

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