美文网首页iOS工作系列iOS && AndroidiOS 好文
干货系列之手把手教你使用Core animation 做动画

干货系列之手把手教你使用Core animation 做动画

作者: Airfei | 来源:发表于2015-12-16 13:18 被阅读5386次

    源码下载:源码

    最近在技术群里,有人发了一张带有动画效果的图片。觉得很有意思,便动手实现了一下。在这篇文章中你将会学到Core Animation显式动画中的关键帧动画、组合动画、CABasicAnimation动画。先上一张原图的动画效果。

    点击此查看[原图动画效果](https://github.com/WZF-Fei/ZFChangeAnimation/blob/master/origin animation.gif)。

    本文要实现的效果图如下:


    实现的效果图.gif

    把原动画gif动画在mac上使用图片浏览模式打开,我们可以看到动画每一帧的显示。从每一帧上的展示过程,可以把整体的动画进行拆分成两大部分。

    第一部分(Part1)从初始状态变成取消状态(图片上是由横实线变成上线横线交叉的圆)。
    第二部分(Part2)从取消状态变回初始状态。

    下面我们先详细分析Part1是怎么实现的。根据动画图,把Part1再细分成三步。

    Step1 : 中间横实线的由右向左的运动效果。这其实是一个组合动画。是先向左偏移的同时横线变短。先看一下实现的动态效果。

    step1 Animation.gif
    ■ 向左偏移---使用基本动画中animationWithKeyPath键值对的方式来改变动画的值。我们这里使用position.x,同样可以使用transform.translation.x来平移。

    ■ 改变横线的大小---使用经典的strokeStartstrokeEnd。其实上横线长度的变化的由strokeStartstrokeEnd之间的值来共同来决定。改变strokeEnd的值由1.0到0.4,不改变strokeStart的值。横线的长度会从右侧方向由1.0倍长度减少到0.4倍长度。参见示意图的红色区域。

    stroke示意图.png
    -(void) animationStep1{
        
        //最终changedLayer的状态
        _changedLayer.strokeEnd = 0.4;
        //基本动画,长度有1.0减少到0.4
        CABasicAnimation *strokeAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
        strokeAnimation.fromValue = [NSNumber numberWithFloat:1.0f];
        strokeAnimation.toValue = [NSNumber numberWithFloat:0.4f];
        //基本动画,向左偏移10个像素
        CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"position.x"];
        pathAnimation.fromValue = [NSNumber numberWithFloat:0.0];
        pathAnimation.toValue = [NSNumber numberWithFloat:-10];
        //组合动画,平移和长度减少同时进行
        CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
        animationGroup.animations = [NSArray arrayWithObjects:strokeAnimation,pathAnimation, nil];
        animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
        animationGroup.duration = kStep1Duration;
        //设置代理
        animationGroup.delegate = self;
        animationGroup.removedOnCompletion = YES;
        //监听动画
        [animationGroup setValue:@"animationStep1" forKey:@"animationName"];
        //动画加入到changedLayer上
        [_changedLayer addAnimation:animationGroup forKey:nil];
    }
    

    Step2 : 由左向右的动画--向右偏移同时横线长度变长。看一下Step2要实现的动画效果。其思路和Step1是一样的。

    step2 Animation.gif
    -(void)animationStep2
    {
        CABasicAnimation *translationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
        translationAnimation.fromValue = [NSNumber numberWithFloat:-10];
        //strokeEnd:0.8 剩余的距离toValue = lineWidth * (1 - 0.8);
    
        translationAnimation.toValue = [NSNumber numberWithFloat:0.2 * lineWidth ];
        
        _changedLayer.strokeEnd = 0.8;
        CABasicAnimation *strokeAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
        strokeAnimation.fromValue = [NSNumber numberWithFloat:0.4f];
        strokeAnimation.toValue = [NSNumber numberWithFloat:0.8f];
        
        CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
        animationGroup.animations = [NSArray arrayWithObjects:strokeAnimation,translationAnimation, nil];
        animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
        animationGroup.duration = kStep2Duration;
        //设置代理
        animationGroup.delegate = self;
        animationGroup.removedOnCompletion = YES;
        [animationGroup setValue:@"animationStep2" forKey:@"animationName"];
        [_changedLayer addAnimation:animationGroup forKey:nil];
    }
    

    Step3: 圆弧的动画效果和上下两个横实线的动画效果。

    1. 画圆弧,首先想到是使用UIBezierPath。画个示意图来分析动画路径。示意图如下:
    step3 示意图.jpg

    整个path路径是由三部分组成,ABC曲线CD圆弧DD′圆
    使用UIBezierPath的方法

    - (void)appendPath:(UIBezierPath *)bezierPath;
    

    把三部分路径关联起来。详细讲解思路。

    ABC曲线就是贝塞尔曲线,可以根据A、B、C三点的位置使用方法

    //endPoint 终点坐标 controlPoint1 起点坐标
    //controlPoint2 起点和终点在曲线上的切点延伸相交的交点坐标
    - (void)addCurveToPoint:(CGPoint)endPoint 
              controlPoint1:(CGPoint)controlPoint1 
              controlPoint2:(CGPoint)controlPoint2;
    

    二次贝塞尔曲线示意图如下:

    二次贝塞尔曲线.png

    其中control point 点是从曲线上取 start point和end point 切点相交汇的所得到的交点。如下图:

    control point .png

    首先C点取圆上的一点,-30°。那么,

    CGFloat angle = Radians(30);
    

    C点坐标为:

        //C点
        CGFloat endPointX = self.center.x + Raduis * cos(angle);
        CGFloat endPointY = kCenterY - Raduis * sin(angle);
    

    A点坐标为:

        //A点 取横线最右边的点
        CGFloat startPointX = self.center.x + lineWidth/2.0 ;
        CGFloat startPointY = controlPointY;
    

    control point 为E点:

        //E点 半径*反余弦(30°)
        CGFloat startPointX = self.center.x + Raduis *acos(angle);
        CGFloat startPointY = controlPointY;
    

    CD圆弧的路径使用此方法确定

    + (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
    

    关于弧度问题,UIBezierPath的官方文档中的这张图:

    弧度.jpg

    StartAngle 弧度即C点弧度,EndAngel弧度即D点弧度。

    CGFloat StartAngle = 2 * M_PI - angle;
    CGFloat EndAngle = M_PI + angle;
    

    DD′圆的路径和上面2一样的方法确定。

    StartAngle 弧度即D点弧度,EndAngel弧度即D′点弧度。

    CGFloat StartAngle = M_PI *3/2 - (M_PI_2 -angle);
    CGFloat EndAngle = -M_PI_2 - (M_PI_2 -angle);
    

    下面部分代码是所有path路径。

        UIBezierPath *path = [UIBezierPath bezierPath];
        
        // 画贝塞尔曲线 圆弧
        [path moveToPoint:CGPointMake(self.center.x +  lineWidth/2.0 , kCenterY)];
        
         CGFloat angle = Radians(30);
        //C点
        CGFloat endPointX = self.center.x + Raduis * cos(angle);
        CGFloat endPointY = kCenterY - Raduis * sin(angle);
        //A点
        CGFloat startPointX = self.center.x + lineWidth/2.0;
        CGFloat startPointY = kCenterY;
        //E点 半径*反余弦(30°)
        CGFloat controlPointX = self.center.x + Raduis *acos(angle);
        CGFloat controlPointY = kCenterY;
    
        //贝塞尔曲线 ABC曲线
        [path addCurveToPoint:CGPointMake(endPointX, endPointY)
                controlPoint1:CGPointMake(startPointX , startPointY)
                controlPoint2:CGPointMake(controlPointX , controlPointY)];
        
        // (360°- 30°) ->(180°+30°) 逆时针的圆弧 CD圆弧
        UIBezierPath *path1 = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.center.x,kCenterY)
                                                             radius:Raduis
                                                         startAngle:2 * M_PI - angle
                                                           endAngle:M_PI + angle
                                                          clockwise:NO];
        [path appendPath:path1];
         // (3/2π- 60°) ->(-1/2π -60°) 逆时针的圆 DD′圆
        UIBezierPath *path2 = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.center.x,kCenterY)
                                                            radius:Raduis
                                                        startAngle:M_PI *3/2 - (M_PI_2 -angle)
                                                          endAngle:-M_PI_2 - (M_PI_2 -angle)
                                                         clockwise:NO];
        
    
        [path appendPath:path2];
    
        _changedLayer.path = path.CGPath;
    

    Path路径有了,接着实现动画效果。
    圆弧的长度逐渐变长。我们还是使用经典的strokeStartstrokeEnd。但是圆弧是如何变长的呢?

    (1) 初始圆弧有一段长度。
    (2) 在原始长度的基础上逐渐变长,逐渐远离A点,同时要在D点停止。
    (3) 长度逐渐变长,最终要在D与D′点交汇。

    我们分别解决这个三个问题。

    第一个问题,strokeEnd - strokeStart > 0这样能保证有一段圆弧。

    第二个问题,逐渐变长,意味着strokeEnd值不断变大。远离A点意味着strokeStart的值不断变大。在D点停止,说明了strokeStart有上限值。

    第三个问题,意味着strokeEnd值不断变大,最终值为1.0。

    这三个问题说明了一个问题,strokeEndstrokeStart是一组变化的数据。

    那么core animation 中可以控制一组值的动画是关键帧动画(CAKeyframeAnimation)。

    为了更准确的给出strokeEndstrokeStart值,我们使用长度比来确定。

    假设我们初始的长度就是曲线ABC的长度。但是贝塞尔曲线长度怎么计算?使用下面方法:

    //求贝塞尔曲线长度
    -(CGFloat) bezierCurveLengthFromStartPoint:(CGPoint)start toEndPoint:(CGPoint) end withControlPoint:(CGPoint) control
    {
        const int kSubdivisions = 50;
        const float step = 1.0f/(float)kSubdivisions;
        
        float totalLength = 0.0f;
        CGPoint prevPoint = start;
        
        // starting from i = 1, since for i = 0 calulated point is equal to start point
        for (int i = 1; i <= kSubdivisions; i++)
        {
            float t = i*step;
            
            float x = (1.0 - t)*(1.0 - t)*start.x + 2.0*(1.0 - t)*t*control.x + t*t*end.x;
            float y = (1.0 - t)*(1.0 - t)*start.y + 2.0*(1.0 - t)*t*control.y + t*t*end.y;
            
            CGPoint diff = CGPointMake(x - prevPoint.x, y - prevPoint.y);
            
            totalLength += sqrtf(diff.x*diff.x + diff.y*diff.y); // Pythagorean
            
            prevPoint = CGPointMake(x, y);
        }
        
        return totalLength;
    }
    

    计算贝塞尔曲线所在的比例为:

    CGFloat orignPercent = [self calculateCurveLength]/[self calculateTotalLength];
    

    初始的strokeStart = 0strokeEnd = orignPercent
    最终的stokeStart = ?

    //结果就是贝塞尔曲线长度加上120°圆弧的长度与总长度相比得到的结果。
    CGFloat endPercent =([self calculateCurveLength] + Radians(120) *Raduis ) / [self calculateTotalLength];
    

    实现动画的代码为

        CGFloat orignPercent = [self calculateCurveLength] / [self calculateTotalLength];
        CGFloat endPercent =([self calculateCurveLength] + Radians(120) *Raduis ) / [self calculateTotalLength];
        
        _changedLayer.strokeStart = endPercent;
        
        //方案1
        CAKeyframeAnimation *startAnimation = [CAKeyframeAnimation animationWithKeyPath:@"strokeStart"];
        startAnimation.values = @[@0.0,@(endPercent)];
        
        CAKeyframeAnimation *EndAnimation = [CAKeyframeAnimation animationWithKeyPath:@"strokeEnd"];
        EndAnimation.values = @[@(orignPercent),@1.0];
        
        
        CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
        animationGroup.animations = [NSArray arrayWithObjects:startAnimation,EndAnimation, nil];
        animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
        animationGroup.duration = kStep3Duration;
        animationGroup.delegate = self;
        animationGroup.removedOnCompletion = YES;
        [animationGroup setValue:@"animationStep3" forKey:@"animationName"];
        [_changedLayer addAnimation:animationGroup forKey:nil];
    

    效果图为:

    step3-1 Animation.gif

    2.上下横线的动画效果。

    此动画效果,需要使用transform.rotation.z转动角度。

    上横线转动的角度顺序为 0 -> 10° -> (-55°) -> (-45°)
    这是一组数据,使用关键帧处理动画。

        CAKeyframeAnimation *rotationAnimation1 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"];
        rotationAnimation1.values = @[[NSNumber numberWithFloat:0],
                                     [NSNumber numberWithFloat:Radians(10) ],
                                     [NSNumber numberWithFloat:Radians(-10) - M_PI_4 ],
                                     [NSNumber numberWithFloat:- M_PI_4 ]
                                     ];
    

    下横线转动的角度顺序为0 -> (-10°) -> (55°) -> (45°)

        CAKeyframeAnimation *rotationAnimation2 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"];
        rotationAnimation2.values = @[[NSNumber numberWithFloat:0],
                                     [NSNumber numberWithFloat:Radians(-10) ],
                                     [NSNumber numberWithFloat:Radians(10)  + M_PI_4 ],
                                     [NSNumber numberWithFloat: M_PI_4 ]
                                     ];
    

    你认为这么就结束了? 最终结束的动画如下:


    step3-2 finished Animation.jpg

    发现相交的直线没有居中,而是靠左显示。

    向左平移,使用transform.translation.x

        //平移量
        CGFloat toValue = lineWidth *(1- cos(M_PI_4)) /2.0;
    

    即旋转角度又发生偏移量,使用组合动画。

    上横线组合动画

         //平移x
        CABasicAnimation *translationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
        translationAnimation.fromValue = [NSNumber numberWithFloat:0];
        translationAnimation.toValue = [NSNumber numberWithFloat:-toValue];
        
        //角度关键帧 上横线的关键帧 0 - 10° - (-55°) - (-45°)
        CAKeyframeAnimation *rotationAnimation1 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"];
        rotationAnimation1.values = @[[NSNumber numberWithFloat:0],
                                     [NSNumber numberWithFloat:Radians(10) ],
                                     [NSNumber numberWithFloat:Radians(-10) - M_PI_4 ],
                                     [NSNumber numberWithFloat:- M_PI_4 ]
                                     ];
      
        
        CAAnimationGroup *transformGroup1 = [CAAnimationGroup animation];
        transformGroup1.animations = [NSArray arrayWithObjects:rotationAnimation1,translationAnimation, nil];
        transformGroup1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
        transformGroup1.duration = kStep3Duration;
        transformGroup1.removedOnCompletion = YES;
        [_topLineLayer addAnimation:transformGroup1 forKey:nil];
    

    下横线组合动画

        //角度关键帧 下横线的关键帧 0 - (-10°) - (55°) - (45°)
        CAKeyframeAnimation *rotationAnimation2 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"];
        rotationAnimation2.values = @[[NSNumber numberWithFloat:0],
                                     [NSNumber numberWithFloat:Radians(-10) ],
                                     [NSNumber numberWithFloat:Radians(10) + M_PI_4 ],
                                     [NSNumber numberWithFloat: M_PI_4 ]
                                     ];
        
    
        CAAnimationGroup *transformGroup2 = [CAAnimationGroup animation];
        transformGroup2.animations = [NSArray arrayWithObjects:rotationAnimation2,translationAnimation, nil];
        transformGroup2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
        transformGroup2.duration = kStep3Duration ;
        transformGroup2.delegate = self;
        transformGroup2.removedOnCompletion = YES;
        [_bottomLineLayer addAnimation:transformGroup2 forKey:nil];
    

    Part1到此结束。最终效果图


    Part1 animation.gif

    Part2的思路和Part1思路是一样的。你可以参考代码自己思考一下。核心代码

    -(void)cancelAnimation
    {
        //最关键是path路径
       
        UIBezierPath *path = [UIBezierPath bezierPath];
        //30度,经过反复测试,效果最好
        CGFloat angle = Radians(30);
        
        CGFloat startPointX = self.center.x + Raduis * cos(angle);
        CGFloat startPointY = kCenterY - Raduis * sin(angle);
        
        CGFloat controlPointX = self.center.x + Raduis *acos(angle);
        CGFloat controlPointY = kCenterY;
        
        CGFloat endPointX = self.center.x + lineWidth /2;
        CGFloat endPointY = kCenterY;
        
        //组合path 路径 起点 -150° 顺时针的圆
        path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.center.x,kCenterY)
                                                             radius:Raduis
                                                         startAngle:-M_PI + angle
                                                           endAngle:M_PI + angle
                                                          clockwise:YES];
        
        
        
        //起点为 180°-> (360°-30°)
        UIBezierPath *path1 = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.center.x,kCenterY)
                                                             radius:Raduis
                                                         startAngle:M_PI + angle
                                                           endAngle:2 * M_PI - angle
                                                          clockwise:YES];
        [path appendPath:path1];
    
        //三点曲线
        UIBezierPath *path2 = [UIBezierPath bezierPath];
    
        [path2 moveToPoint:CGPointMake(startPointX, startPointY)];
        
        [path2 addCurveToPoint:CGPointMake(endPointX,endPointY)
                 controlPoint1:CGPointMake(startPointX, startPointY)
                 controlPoint2:CGPointMake(controlPointX, controlPointY)];
        
        [path appendPath:path2];
        
        //比原始状态向左偏移5个像素
        UIBezierPath *path3 = [UIBezierPath bezierPath];
        [path3 moveToPoint:CGPointMake(endPointX,endPointY)];
        [path3 addLineToPoint:CGPointMake(self.center.x - lineWidth/2 -5,endPointY)];
        [path appendPath:path3];
        
        _changedLayer.path = path.CGPath;
        
        //平移量
        CGFloat toValue = lineWidth *(1- cos(M_PI_4)) /2.0;
        //finished 最终状态
        CGAffineTransform transform1 = CGAffineTransformMakeRotation(0);
        CGAffineTransform transform2 = CGAffineTransformMakeTranslation(0, 0);
        CGAffineTransform transform3 = CGAffineTransformMakeRotation(0);
        
        CGAffineTransform transform = CGAffineTransformConcat(transform1, transform2);
        _topLineLayer.affineTransform = transform;
        transform = CGAffineTransformConcat(transform3, transform2);
        _bottomLineLayer.affineTransform = transform;
        
        //一个圆的长度比
        CGFloat endPercent = 2* M_PI *Raduis / ([self calculateTotalLength] + lineWidth);
    
        
        //横线占总path的百分比
        CGFloat percent = lineWidth / ([self calculateTotalLength] + lineWidth);
        
        _changedLayer.strokeStart = 1.0 -percent;
        
        CAKeyframeAnimation *startAnimation = [CAKeyframeAnimation animationWithKeyPath:@"strokeStart"];
        startAnimation.values = @[@0.0,@0.3,@(1.0 -percent)];
        
        //在π+ angle
        CAKeyframeAnimation *EndAnimation = [CAKeyframeAnimation animationWithKeyPath:@"strokeEnd"];
        EndAnimation.values = @[@(endPercent),@(endPercent),@1.0];
        
        
        CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
        animationGroup.animations = [NSArray arrayWithObjects:startAnimation,EndAnimation, nil];
        animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
        animationGroup.duration = kStep4Duration;
        animationGroup.delegate = self;
        animationGroup.removedOnCompletion = YES;
        [animationGroup setValue:@"animationStep4" forKey:@"animationName"];
        [_changedLayer addAnimation:animationGroup forKey:nil];
        
        //平移x
        CABasicAnimation *translationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
        translationAnimation.fromValue = [NSNumber numberWithFloat:-toValue];
        translationAnimation.toValue = [NSNumber numberWithFloat:0];
        
        //角度关键帧 上横线的关键帧  (-45°) -> (-55°)-> 10° -> 0
        CAKeyframeAnimation *rotationAnimation1 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"];
        rotationAnimation1.values = @[[NSNumber numberWithFloat:- M_PI_4 ],
                                      [NSNumber numberWithFloat:- Radians(10) - M_PI_4 ],
                                      [NSNumber numberWithFloat:Radians(10) ],
                                      [NSNumber numberWithFloat:0]
                                      ];
        
    
        CAAnimationGroup *transformGroup1 = [CAAnimationGroup animation];
        transformGroup1.animations = [NSArray arrayWithObjects:rotationAnimation1,translationAnimation, nil];
        transformGroup1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
        transformGroup1.duration = kStep4Duration;
        transformGroup1.removedOnCompletion = YES;
        [_topLineLayer addAnimation:transformGroup1 forKey:nil];
        
        //角度关键帧 下横线的关键帧  (45°)-> (55°)- >(-10°)-> 0
        CAKeyframeAnimation *rotationAnimation2 = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"];
        rotationAnimation2.values = @[[NSNumber numberWithFloat: M_PI_4 ],
                                      [NSNumber numberWithFloat:Radians(10) + M_PI_4 ],
                                      [NSNumber numberWithFloat:-Radians(10) ],
                                      [NSNumber numberWithFloat:0]
                                      ];
    
        CAAnimationGroup *transformGroup2 = [CAAnimationGroup animation];
        transformGroup2.animations = [NSArray arrayWithObjects:rotationAnimation2,translationAnimation, nil];
        transformGroup2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
        transformGroup2.duration = kStep4Duration;
        transformGroup2.delegate = self;
        transformGroup2.removedOnCompletion = YES;
        [_bottomLineLayer addAnimation:transformGroup2 forKey:nil];
        
    }
    

    最终效果图:


    finished animation.gif

    本篇文章讲解结束!

    代码点此链接下载:https://github.com/WZF-Fei/ZFChangeAnimation

    相关文章

      网友评论

      本文标题:干货系列之手把手教你使用Core animation 做动画

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