手把手教你使用Core animation 做动画(上)

手把手教你使用Core animation 做动画(上)


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


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



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


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

第二部分(Part2)从取消状态变回初始状态。


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


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



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


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

-(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是一样的。



-(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: 圆弧的动画效果和上下两个横实线的动画效果。


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

整个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;


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

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


首先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的官方文档中的这张图:



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路径有了,接着实现动画效果。

圆弧的长度逐渐变长。我们还是使用经典的strokeStart和strokeEnd。但是圆弧是如何变长的呢?


(1) 初始圆弧有一段长度。

(2) 在原始长度的基础上逐渐变长,逐渐远离A点,同时要在D点停止。

(3) 长度逐渐变长,最终要在D与D′点交汇。

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


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


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


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


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


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


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


假设我们初始的长度就是曲线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 = 0、strokeEnd = 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];


效果图为:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值