点击打开链接

http://www.jianshu.com/p/272aa1f26c62

动画和绘图是iOS开发中非常重要的部分。我们要实现一个动效,首先就是动画解析,分析动画的路径,然后再考虑具体的代码。这需要一定的经验和Core Animation、Core Graphics等相关知识的基础。本文通过三个动画的demo来分析动画的实现。这几个demo已经放到了github上面,地址是:https://github.com/daixunry/DribbbleAnimationPractice

一、一个有趣的旋转动画

  这个旋转动画,是一个来自于dribbble的设计,非常的可爱,地址是:https://dribbble.com/shots/2064446-Refresh 看到的第一眼,我就打算把它实现了

refresh.gif

拆解和分析

  这是一个结构比较简单的动画,它的点在于两个箭头不停的旋转,当旋转到一半的时候,会改变箭头的方向。

  一般情况下,会使用路径(path)来绘制整个圆弧和箭头,因为这样方便给箭头的方向改变添加动画;一共需要两组动画,一组是整个视图旋转,另一组是修改箭头方向的动画,这个动画可以在用于绘制箭头的CAShapeLayer上添加CAKeyframeAnimation,对layer的path属性做动画。

代码实现

  详细的代码可见
https://github.com/daixunry/DribbbleAnimationPractice。

  首先是绘制的思路:

    1、两段圆弧,分别是2个CAShapeLayer;2、两个箭头,也分别是2个CAShapeLayer;3、创建一个view,把这几个CAShapeLayer作为子layer添加进来;4、给这几个CAShapeLayer添加path属性,也就是绘制的路径。

  绘制完成之后开始做动画:

  第一个动画是给整个视图做旋转,非常简单,方法有很多种,我这里面用的是CABasicAnimation,对它们共同的父layer的transform.rotation.z属性做动画。layer的transform是一个CATransform3D类型的属性,rotation.z就是绕着Z轴旋转。

  第二个是修改箭头layer的path动画。

    CAKeyframeAnimation *aniChangePath1 = [CAKeyframeAnimation animationWithKeyPath:@"path"];aniChangePath1.values = @[(__bridge id)_startArrowPath1.CGPath,(__bridge id)_endArrowPath1.CGPath,(__bridge id)_endArrowPath1.CGPath];aniChangePath1.keyTimes = @[@(0.45),@.75,@.95];aniChangePath1.autoreverses = YES;aniChangePath1.repeatCount = NSIntegerMax;aniChangePath1.duration = 1;

  我们可以观察箭头变换动画:整个先旋转半圈,然后箭头才开始变换方向,然后又变回来。那么CAKeyframeAnimation的values,路径初始值我们设定为箭头的初始path,在keyTimes设定先维持0.45s,也就是等旋转一段时间后才开始变化箭头的路径,这个也可以通过设置动画的beginTime实现,方法比较多,不多鳌述。我们设置autoreverses属性为YES,就是动画完成之后自动反转,在回到初始状态,往复循环。

  就这样,这个简单的动画就实现了。但是其实动画的实现方式很多,这个方式也是可以优化的。

优化

  CAReplicatorLayer:复制图层。这个图层非常有趣,这里简单的介绍一下。它可以实现一种效果:复制它的subLayer,并做一些有规律的调整,我们看一些属性:

    //指定实体的个数,如果为10,那么它的每个subLayer会被复制,都拥有10个实例instanceCount  //修改复制出来的layer的动画时钟的延迟//通俗的讲,假如delay为1s,那么就会有这样的效果,如果我往它的sublayer添加一个动画,//这个sublayer的第一个副本的动画开始时间会是它本身1s之后执行,第二个副本的动画就是2s之后,依次类推 instanceDelay               //这个属性是CATransform3D类型,用来依次设置每一个复制的layer副本的偏移量//举个例子,假如该属性的值是CATransform3DMakeTranslation(100, 0, 0),第一个subLayer的x坐标假如是0,//第二个sublayer的x坐标就是100,依次类推instanceTransform           //同上面的其他属性相似,设置颜色的偏差instanceColor               ...

  看到这里,我们就大概能猜到如何优化上面这个动画。对,我们利用复制图层,减少绘图和动画一半的代码量。设置复制图层的核心代码就是:

    _parLayer = [CAReplicatorLayer layer];_parLayer.frame = self.bounds;_parLayer.instanceCount = 2;_parLayer.instanceTransform = CATransform3DMakeRotation(M_PI, 0, 0, 1);

  instanceCount为2,就是存在2个sublayer的实例,instanceTransform的值是沿着z轴旋转180度,我们往这个图层只添加一个圆弧和箭头,通过180度的翻转复制,就可以完成绘制。复制图层有一个关键的地方是,你往原始的图层中添加动画效果,复制出来的副本可以实时的复制这些动画效果。

二、网易新闻个人页面的水波效果

  网易新闻客户端的这个水波效果出来很久了,我考虑了很长时间该如何实现,但是都没有很好的办法,幸好在一个动画牛人kittenyang的帮助下,知道了实现这个动画最关键的点。他的blog上面有许多优秀的动画案例,非常值的学习,blog的地址是http://kittenyang.com/

waveGif.gif

  这个动画的关键点就是正余弦函数。在听到这个的时候,我非常的震惊,原因是正余弦我们当初在高中的时候学习的知识,不过从来没有想过这些高中书本的知识竟然运用到了实际,非常的佩服kittenyang,同时感觉非常羞愧的,高中的知识都还给老师了,连正余弦的公式都忘记了。不熟悉的同学也可以去复习一下。

正弦型函数解析式:y=Asin(ωx+φ)+h
各常数值对函数图像的影响:
φ(初相位):决定波形与X轴位置关系或横向移动距离(左加右减)
ω:决定周期(最小正周期T=2π/|ω|)
A:决定峰值(即纵向拉伸压缩的倍数)
h:表示波形在Y轴的位置关系或纵向移动距离(上加下减)

拆解和分析

  好了,我们还是来拆解一下这个动画吧。两个波浪是两个正弦函数的效果叠加。首先我们看看该如何绘制一个波的曲线,如下图:

001

  我们知道,计算机不可能绘制出一条完美的曲线,如果放大到像素的级别,可以看到这些曲线其实都是栅格的像素点组成。我们只能最大化的接近曲线,达到肉眼无法分辨的程度。如果想绘制出来一条正弦函数曲线,可以沿着假想的曲线绘制许多个点,然后把点逐一用直线连在一起,如果点足够多,就可以得到一条满足需求的曲线,这也是一种微分的思想。而这些点的位置可以通过正弦函数的解析式求得。

  如果要绘制上面这个曲线,可以观察:波的峰值是1,周期是2π,初相位是0,h位移也是0。那么计算各个点的坐标公式就是y = sin(x);获得各个点的坐标之后,使用CGPathAddLineToPoint这个函数,把这些点逐一连成线,就可以得到最后的路径。

  接下来问题来了,我们已经绘制了一条静态的曲线,如何让它形成一个流动的波呢?

  可以这么思考:初始的曲线如上面所示,1s之后,希望曲线能成为下个形态:

002

  接着,2s、3s...,曲线分别在不停的变化,如下图:

003

  那么随着时间的流逝,这个曲线在不停的起伏变化,就形成了波动的效果。我们认真的想想,波动其实就是每一个点的y坐标都在不停的做着周期变化,想要实现上图1s之后的曲线形态,需要设置上面公式中的φ常量(初相位),假如φ是π/2,那么y=sin(x+φ)在x=0位置的时候,y的值就不在是0,而是1,就得到一条变化的曲线。通过上面的分析,我们知道,需要建立一个时间和φ的函数。

  我们可以创建一个定时器(当然做动画我们肯定不会使用计时器,这里举个例子,下面详解),假设每秒让φ自增π/2,这样第4s的时候,φ等于2π(一个周期),y=sin(x+2π)和y=sin(x)等效,又回到了初初始状态,这样就完成了一个波动周期,往下继续加下去,不停的往复这个波动周期动画。

  如果我们希望波动的非常剧烈,也就是波流速很快,那么我们可以让初相位随着时间的函数波动更快,就可以实现了。

代码实现

  把上面的原理落实到我们需要制作的动画上面。首先要总结出一个公式,确定正弦型函数解析式:y=Asin(ωx+φ)+h中各个常数的值。这里需要注意UIKit的坐标系统y轴是向下延伸。

    1、我们的容器高度是100,我希望波的整体高度,固定在容器的一个相对的位置。这里设置h = 30;也就是说,当Asin(ωx+φ)计算为0的时候,这个时候y的位置是30;2、决定波起伏的高度,我们设置波峰是5,波峰越大,曲线越陡峭;3、决定波的宽度和周期,比如,我们可以看到上面的例子中是一个周期的波曲线,一个波峰、一个波谷,如果我们想在0到2π这个距离显示2个完整的波曲线,那么周期就是π。我们这里设置波的宽度是容器的宽度_waveWidth,希望能展示2.5个波曲线,周期就是_waveWidth/2.5。那么ω常量就可以这样计算:2.5*M_PI/_waveWidth。4、一共有两个波曲线,形成一个落差,也就是设置不同的φ(初相位),我们这里设置落差是M_PI/4。5、时间和初相位的函数关系:我们在计时器的函数中一直调用_offset += _speed;可以看到,如果我们设置波的速度speed越大,波的震动将会越快。最后我们的公式如下:CGFloat y = _waveHeight*sinf(2.5*M_PI*i/_waveWidth + 3*_offset*M_PI/_waveWidth + M_PI/4) + _h;这些参数都可以自己调整,得到一个符合要求的效果。

  现在我们解决了项目中最有难度的问题,剩下的事情就非常简单了。两个波是两个CAShapeLayer。我们使用CADisplayLink而不是计时器来驱动动画,因为CADisplayLink触发的时机是每隔一帧运行一次,而NSTimer不是很精确,会有阻塞的情况,照成动画卡顿的现象。

- (void)wave
{_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(doAni)];[_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}- (void)doAni
{_offset += _speed;//设置第一条波曲线的路径CGMutablePathRef pathRef = CGPathCreateMutable();//起始点CGFloat startY = _waveHeight*sinf(_offset*M_PI/_waveWidth);CGPathMoveToPoint(pathRef, NULL, 0, startY);//第一个波的公式for (CGFloat i = 0.0; i < _waveWidth; i ++) {CGFloat y = 1.1*_waveHeight*sinf(2.5*M_PI*i/_waveWidth + _offset*M_PI/_waveWidth) + _h;CGPathAddLineToPoint(pathRef, NULL, i, y);}CGPathAddLineToPoint(pathRef, NULL, _waveWidth, 40);CGPathAddLineToPoint(pathRef, NULL, 0, 40);CGPathCloseSubpath(pathRef);//设置第一个波layer的path_layer.path = pathRef;_layer.fillColor = [UIColor lightGrayColor].CGColor;CGPathRelease(pathRef);//设置第二条波曲线的路径CGMutablePathRef pathRef2 = CGPathCreateMutable();CGFloat startY2 = _waveHeight*sinf(_offset*M_PI/_waveWidth + M_PI/4);CGPathMoveToPoint(pathRef2, NULL, 0, startY2);//第二个波曲线的公式for (CGFloat i = 0.0; i < _waveWidth; i ++) {CGFloat y = _waveHeight*sinf(2.5*M_PI*i/_waveWidth + 3*_offset*M_PI/_waveWidth + M_PI/4) + _h;CGPathAddLineToPoint(pathRef2, NULL, i, y);}CGPathAddLineToPoint(pathRef2, NULL, _waveWidth, 40);CGPathAddLineToPoint(pathRef2, NULL, 0, 40);CGPathCloseSubpath(pathRef2);_layer2.path = pathRef2;_layer2.fillColor = [UIColor lightGrayColor].CGColor;CGPathRelease(pathRef2);
}

  我们可以看到,两个波曲线不但初相位不同,形成一个落差,而且相位随着时间的改变速度也不同,带来两个波的流速不同的视觉差异。CADisplayLink每帧都会调用wave方法,wave不停的改变着offset的值,也就是改变着初相位,最后形成了波动动画。

三、3D sideBar

  这是一个一度比较火的3D sideBar效果,我先是在微博上看到有人在发这个gif图,私下就抽时间完成了这个效果,后来发现它来自与http://www.raywenderlich.com/87268/3d-effect-taasky-swift ,是一个用swift语言实现的版本,但是跟我的是完全不同的实现思路。

sideBar3d.gif

  总体而言,这个动画的实现难度最大。因为它对Core Animation和Core Graphics有一定的要求。我们可以来看一下它有哪些困难的地方。

拆解和分析

  要解决的问题总结一下,大体上有下面几个:

  1、黄色sidebar区域的3D翻转效果。

  2、黄色sidebar区域翻转的同时有阴影渐变效果。

  3、黄色sidebar区域翻转的同时,绿色的contentView跟随右推的效果。

  4、这个靠手势驱动的动画,在手势结束的时候,继续自动完成翻转动画的任务。

  下面我们来逐一解决这些难点:

  1、黄色sidebar区域的3D翻转效果:

  我们可以尝试一下,自己做一个简单的翻转动画。可能会遇到的问题有:
系统默认的翻转是沿着视图的中心位置,如何沿着sidebar区域的右侧翻转?
如何有3D立体的效果?在翻转的同时,如何保持sidebar区域的左侧一直贴着屏幕的左侧?

  沿着sidebar区域的右侧翻转比较简单,设置layer的anchorPoint为(1,0.5)即可。

  3D翻转效果,需要这样设置:

    CATransform3D tran = CATransform3DIdentity;tran.m34 = -1/500.0;

  iOS中的CALayer的3D本质上并不能算真正的3D,而只是3D在二维平面上的投影,m34的值添加了视点来决定平面投影,它的效果就是在投影平面上表现出近大远小,具体的原理可见 http://geeklu.com/2012/07/ios-3d-perspective/ 这一篇blog讲的特别好。

  在给sidebar应用翻转变换的时候,需要加上这个效果,例如我们在初始化的时候,需要菜单被折叠起来,那么是这样设置的:

    //contaTran沿Y轴翻转是在tran的基础之上CATransform3D contaTran = CATransform3DRotate(tran,-M_PI_2, 0, 1, 0);//初始的位置是被折叠起来的,也就是上面的contaTran变换是沿着右侧翻转过去,但是我们需要翻转之后的位置是贴着屏幕左侧,于是需要一个位移CATransform3D contaTran2 = CATransform3DMakeTranslation(-self.frame.size.width, 0, 0);//两个变换的叠加_containView.layer.transform = CATransform3DConcat(contaTran, contaTran2);

  翻转的同时,保持sidebar区域的左侧一直贴着屏幕的左侧:我们需要创建一个辅助视图,在动画中,创建辅助视图是比较常规的手段。

  之所以这么做的原因是,翻转的时候,因为是沿着sidebar的右侧翻转,所以它的右侧是不动的。我们需要添加一个位移,这个位移可以保证翻转的同时,还在往右边移动,保持sidebar的左侧一直贴着屏幕的左侧,制造一种sidebar的左侧不动,右侧移动的效果。这个时候可以利用辅助视图,让这个辅助视图的大小和transform等属性和sidebar一致,再把翻转变换真正应用在sidebar之前,我们先把翻转应用到辅助视图上面,这个时候计算出当前辅助视图的宽度,这个宽度也是sidebar即将要旋转的时候的宽度,利用sidebar的总宽度减去辅助视图的宽度,就是sidebar需要位移的距离。这一点可以亲手实践,细细体会。

  代码如下:

    //翻转矩阵CATransform3D contaTran = CATransform3DRotate(tran,-M_PI_2 + rota, 0, 1, 0);//先应用到辅助视图上面_containHelperView.layer.transform = contaTran;//根据辅助视图计算sidebar需要的位移矩阵CATransform3D contaTran2 = CATransform3DMakeTranslation(_containHelperView.frame.size.width - 100, 0, 0);//两个变换的叠加_containView.layer.transform = CATransform3DConcat(contaTran, contaTran2);

  2、黄色sidebar区域翻转的同时有阴影渐变效果:

  这个相对比较容易实现一点,渐变阴影使用单独的CAGradientLayer,添加到sidebar的子图层中,然后做CAGradientLayer的colors动画。

  3、黄色sidebar区域翻转的同时,绿色的contentView跟随右推的效果:

  经过刚才第一个问题的分析,我们知道,可以利用辅助视图解决这个问题。当前的辅助视图的宽度,就是绿色的contentView的位移距离。

    self.containerView.transform = CGAffineTransformMakeTranslation(_containHelperView.frame.size.width, 0);

  4、这个靠手势驱动的动画,在手势结束的时候,继续自动完成翻转动画的任务:

  我们设定一个阀值,手势结束的时候,判断当前是把动画做完,还是做撤销动画,比如打开的动作,还没有做到一半,需要添加动画效果,恢复到之前的状态。

代码实现

  其实上面的拆解和分析,已经把核心的代码都讲了,这里简单总结一下。

  1、首先就是动画是靠手势来驱动的,根据pan手势的位移,控制动画的进度,例如,我们希望手势移动100point的时候,动画可以做完,那么就使用位移和100point的比率,来计算现在的变换矩阵。

  2、根据当前的状态,是打开还是关闭菜单,和进度,来决定渐变阴影的深度,越是接近要打开的状态,阴影就会变浅,消失,越是要折叠起来,阴影越深。

  3、菜单的翻转,和内容视图的推移动画的代码上面分析过了。手势结束的时候,做完剩下的动画,因为之前一直在用手势驱动计算变换矩阵,也就是说,并没有在各个视图上添加动画对象,而是不停的改变他们的transform属性。当手势结束的时候,要添加一个动画上来,完成剩余的动作:

    CABasicAnimation *tranAni = [CABasicAnimation animationWithKeyPath:@"transform"];tranAni.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];tranAni.fromValue = [NSValue valueWithCATransform3D:_containView.layer.transform];tranAni.toValue = [NSValue valueWithCATransform3D:tran];tranAni.duration = .5;[_containView.layer addAnimation:tranAni forKey:@"openForContainAni"];_containView.layer.transform = tran;

优化

  我们前一种实现的方法,先是用手势驱动,然后在添加动画的组合方式,显得有一些乱。这里面,我们利用layer的一个属性speed可以进行优化。

  layer的speed属性默认值是1,如果设置为2的话,那么动画的速度会提高一倍,如果设置为0的话,动画不会进行,处于停止状态。

  layer还有一个属性,timeOffset,用来控制当前视图的状态处于动画的什么位置。举个例子:如果我们的speed设置为0,timeOffset设置为0.5,当前的视图就会呈现动画执行到一半的时候的视图状态。

  这样,我们只需要在前期设置好各个视图的动画,把layer的speed设置为0,在根据手势的进度,设置layer的timeOffset。

  不过我们需要注意两个问题,一个是手势结束我们需要在设置speed为1的时候,需要获取当前的视图Presentation tree的transform,并且更新到model tree,很简单,代码如下:

    _containerView.layer.transform = [[_containerView.layer.presentationLayer valueForKeyPath:@"transform"] CATransform3DValue];

  还有一个问题是,我们给model tree赋值的时候,默认会有隐式动画的效果,我们需要禁止这种行为:

    [CATransaction setDisableActions:YES];

  动画分析就说到这里,详细的细节可以参考源码。

文/daixunry(简书作者)
原文链接:http://www.jianshu.com/p/272aa1f26c62
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

iOS核心动画--浅析三个动画的实现相关推荐

  1. Silverlight动画基础三:动画与向量-模拟重力效果

    这次将使用向量和动画结合来模拟物体在具有重力下的运动效果,主要逻辑: 1.定义小球的重力,摩擦力.以及运动速度的递减变量 2.根据鼠标拖拽小球的前后位置的差值来设置小球的起始速度,拖动越快速度也就越大 ...

  2. react native学习笔记29——动画篇 Animated高级动画

    1.前言 上一节我们学习了全局的布局动画api--LayoutAnimation,体验到其流畅柔和的动画效果,但有时我们需要实现一些更精细化的动画,或者完成一些组合动画,这时我们可以使用React N ...

  3. Android动画框架(二)----属性动画

    转载请注明出处:http://blog.csdn.net/fishle123/article/details/50705928 Android提供三种形式动画:视图动画,帧动画,属性动画.其中属性动画 ...

  4. 高级UI- 属性动画炫酷动画案例+淘宝动画+源码解析+策略模式使用

    文章目录 属性动画源码: 案例1 案例2 最终效果 思路 : 代码 TODU 案例3 加载的炫酷动画. 以及策略模式的使用 效果图 思路 动画分析 先实现小圆的旋转动画, 开始在ondraw里面写动画 ...

  5. iOS 核心动画 Core Animation浅谈

    代码地址如下: http://www.demodashi.com/demo/11603.html 前记 关于实现一个iOS动画,如果简单的,我们可以直接调用UIView的代码块来实现,虽然使用UIVi ...

  6. iOS核心动画Core Animation(一)

    核心动画Core Animation(一) 一.简述 Core Animation是直接作用在CALayer上的(并非UIView上)非常强大的跨Mac OS X和iOS平台的动画处理API,Core ...

  7. iOS核心动画以及UIView动画的介绍

    我们看到很多App带有绚丽狂拽的特效,别出心裁的控件设计,很大程度上提高了用户体验,在增加了实用性的同时,也赋予了app无限的生命力.这些华丽的效果很多都是基于iOS的核心动画原理实现的,本文介绍一些 ...

  8. iOS开发系列--让你的应用“动”起来--超详细的ios核心动画介绍

    概览 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌.在这里你可以看到iOS中如何使用图层精简非交互式绘图,如何通过核心动画创建基础动画.关键帧动画 ...

  9. iOS核心动画学习整理

    最近利用业余时间终于把iOS核心动画高级技巧(https://zsisme.gitbooks.io/ios-/content/chapter1/the-layer-tree.html)看完,对应其中一 ...

最新文章

  1. SQLAlchemy简介与入门
  2. KOMODO:用16S rDNA序列预测其培养基配方的网站
  3. 03、Swagger2和Springmvc整合详细记录(爬坑记录)
  4. CNN 中1X1卷积核的作用
  5. 牛客网(剑指offer) 第十六题 合并两个排序的链表
  6. Idea Spring Boot配置文件.yaml或.properties不能自动提示的有效解决办法
  7. 洛谷 P1736 创意吃鱼法
  8. 项目部署服务器 jstl,Springboot集成jsp及部署服务器实现原理
  9. linux mq脚本,Linux自动化命令工具expect
  10. asp.net分页方法
  11. 朱光潜:要有悲剧,才能算人生
  12. spring中用到了哪些设计模式
  13. 基于CXF的WebService的安全验证问题
  14. 自己收集的全国行政区划,具体到县级,不包括过直辖市和特别行政区
  15. scala安装与使用
  16. OpenGL纹理叠加基础知识
  17. Microsoft Designer 微软设计师 拆机(拆鼠标)路程
  18. 怎么查二手苹果手机价格
  19. 1分钟学会网站采集方法详解
  20. NVIDIA边缘端设备(nano、tx2、xavier等)查看安装的jetpack版本

热门文章

  1. COLORREF结构
  2. 从Java Future 到 Guava ListenableFuture实现异步非阻塞调用
  3. 泛微华南牵手深圳前海第三方供应链数据方案有限公司
  4. linux字符图形命令,神奇的 Linux 命令行字符形状工具 boxes | Linux 中国
  5. 财务报表,with用法, 科目余额
  6. zabbix源码编译
  7. hack the box:silo靶机
  8. cmw测试ble_如何使用MT8852B来测试低功耗蓝牙
  9. 0909币安测试开发面试题
  10. 人工智能3d建模算法_基于计算视觉和3D建模 智能3D带来无限可能