本篇是 Jics 的投稿,分享如何分解一个动画效果,来快速实现它。分析的思路和效果都非常好,这只是第一篇,希望大家喜欢。
Jics 的博客地址:
http://www.jianshu.com/p/3dd3d1524851
此篇中的小鱼动画是模仿国外一个大牛做的flash动画,第一眼就爱上它了,简约灵动又不失美学,于是抽空试着尝试了一下。
如下是我用Android实现的效果图:
由于整个绘制分析过程比较繁琐所以灵动的红鲤鱼准备做成上下两篇,本篇是小鱼儿绘制的实现篇,第二篇是小鱼儿游动控制篇。
本篇实现如下效果:
绘制实现篇用到如下主要的技术:
1、自定义 Drawable 动画。
2、Android 的坐标及角度。
3、Canvas 中 Layer 的使用。
4、正余弦函数的使用以及角度角和弧度角的转换。
下图是我实现小鱼儿的分解图纸:
拿到动画需求或者模仿一个动画首先需要分析动画主体如何绘制部件如何活动,就此动画外观分析如下:
小鱼的身体各个部件都是简单的半透明几何图形。
各个部件都可以活动。
从头到尾方向的部件摆动幅度越来越大、频率越来越高。
小鱼摆动是周期运动,三角函数正好有此特性,角度问题也需要和坐标挂钩,所以我们先来明确一下两个最重要也是最基本的问题:坐标和角度。与平面直角坐标系不同的是 Android的坐标系中Y轴正方向是朝下的 ,但是角度却和平面直角坐标系的计算方法一样,即 原点指向X轴正方向为0°,正角度是逆时针旋转,负角度是顺时针旋转 。
那么问题就来了:坐标系不同,角度转动方式却一样,为了让 Java 中的 Math 函数计算出来的角度跟 Android 的坐标习惯一致,我们需要将与Y轴相关的角度都减去 180°,这样解决了既用 Android 的坐标又用自然角度的问题,即下图所示的角度和坐标系关系。
统一完角度问题,接下来我们就看看鱼的各部件是怎么关联在一起的。
需要先了解三个重要参数。
因为最终我们要实现鱼儿根据手指点击的位置而移动的效果,必须确保能让点击点成为唯一确定鱼儿位置的点,所以我们必须找到一个让鱼儿的各个部件都相对此点绘制的点。
参考点可以任意选,但是考虑到转弯的时候或者身体摆动的时候不会往某一边偏,于是将参考点选在鱼的中轴线上,本来选在中轴线和鱼儿头顶橡胶的点但是最后转弯的时候就跟秋名山老司机漂移一样,那叫一个飘逸,最后将参考点选在了鱼的腹部重心处。
此案例中鱼的各个部件都是以 鱼头半径R为单位 衡量的,比如鱼的身子第一节长度是 3.2R ,依次确定好身体的各个部件相对于鱼头半径的尺寸就能确定整条鱼的总长度为6.79R,继而确定控件的总尺寸。如下图,经过计算控件最小尺寸为 8.36R ,这样就保证鱼儿转动任意角度都在控件之内。
此处的鱼身角度是指重心到鱼头圆心的连线和X轴正方向的夹角角度,即鱼儿前进方向的角度。此方向是确定各个部件方向及位置的的基础方向,部件的定位、鱼身角度以及尾部的摆动角度都是在此角度基础上通过加减角度来控制左右摇摆。
下边我将演示一下如何通过这三个因素来确定头部以及鱼鳍的点坐标(其他部位原理相同)。
先假设鱼身角度为 0° ,即头朝向X轴正方向。通过重心点以及第一节身长的一半的长度,以及角度即可计算出头部的圆心坐标,然后再以头部圆心坐标和 0.9R 的长度,顺时针旋转 80° 确定右边鱼鳍的坐标点。
鱼鳍绘制原理相似,通过上文的右鳍坐标可以计算出右鳍的另一端坐标,鱼鳍弧度是通过二阶贝塞尔曲线绘制的。
鱼尾张合分析。鱼尾是内外两个三角形叠加而成的,三角形顶点和三角形底边中点连线的角度和最后一节身体的角度一直,三角形底边左右两点通过底边的中点以及动态计算出来的长度确定的。
后用放出骨架系统:黑线为各个部件的主轴,圆圈为各个部件边界的定位点或贝塞尔曲线的控制点,是不是很酷,像不像电影里的动作捕捉。
文章只贴出主要代码,完整代码文末提供链接。
自定义View可能大家都知道,但是自定义 Drawable 却并不是很常见。我们知道 Drawable 在 Android 里常常和 ImageView 配合使用,或者作为某个 View 的 Background ,它不能通过标签的方式在 XML 里定义,所以严格意义上来说它不是一个可以独立展示的控件,需要依附在其他控件中。在 attrs.xml 里自定义属性也和它无缘, measure 测量也可以省略,这么一看 Drawabe 好像就只是专著绘制,没错,这就是它比 View 和 ViewGroup 绘图的优势 —— 轻量。
既然说到不用 Measure ,那么它的大小怎么确定呢?
当 ImageView 使用我们自定义 Drawable 的时候,如果设置的是 wrap_content ,那么 content 的内容宽高从哪里来? Drawable 提供了两个函数 getIntrinsicHeight()
、getIntrinsicWidth()
,从名字上看是获得固有宽高,所以我们就可以在这里控制我们的 Drawable 本来的宽高。如果 ImageView 的宽高是具体值的话,具体值超过 Drawable 的固有宽高,那么 Drawable 就会被拉伸(具体拉伸方案是依据 ImageView 的 scaleType 类型),如果不想让自己的内容因拉伸而导致不清晰的话可以在draw()
函数里通过 canvas.getHeight()和canvas.getWidth() 来获取 ImageView 的大小。也可以通过 getBounds()
方法获取到一个Rect边界来获取尺寸。
本例中的固有宽高就是可以容纳小鱼 360° 旋转的尺寸 8.38R 。
其次自定义 Drawable 只需复写必要的四个函数,比较简单具体作用见注释。
主要是复写 draw()
方法,利用 Canvas 绘制各种想要的东西。
最最最主要的坐标计算代码,小鱼儿所有部件都是通过此方法计算出坐标的 ,功能是计算一个点的坐标,可以理解为一个长度为 length 的线绕起点 startPoint 旋转 angle 角度后线段另一端的坐标。
这里要特别说明一下 Math.sin()、Math.cos()、Math.toRadians() 这三个函数,其中 sin\cos 的参数是弧度制角度。说到弧度制可能大家都忘得差不多了,带大家回顾一下中学数学。角的度量可以用弧度制也可以用角度制表示。其中弧度和角度转换的桥梁就是圆周率π。
1角度=(π/180)弧度
比如说想计算 30° 的正弦值,用 Java 代码需要先将角度制的30°转为弧度值即通过Math.toRadians(30)
得到 30° 对应的弧度,完整代码如下:
double sin30 = Math.sin( Math.toRadians(30) );
打印结果是:
0.49999999999999994
如果非要得到0.5的话就强转成 floa t型就行了,可能是由于 double 的精度问题。
第一节身体包括头部和身体的第一段,代码如下(虚线部分是身体其他部分的生成方法,暂时不管)。
其中最难理解的是角度的计算这句话:
float angle = mainAngle + (float) Math.sin(Math.toRadians(currentValue * 1.2 * waveFrequence)) * 2;//中心轴线和X轴顺时针方向夹角
这里Math.sin(Math.toRadians(currentValue * 1.2 * waveFrequence))
是控制第一节身体摆动的核心方法,变量currentValue
是 ValueAnimator 动画的过程数值, 1.2 是用来控制身体摆动的固有频率, waveFrequence 是全局频率,用于控制鱼儿运动时的摆动频率,因为 sin函数 是周期函数,且值域为 [-1,1] ,计算结果乘2之后这句话就可以生成一个 [-2,2] 的变化范围,用这个值加上 mainAngle(身体前进方向和X轴正方向夹角)就可以让鱼的第一节身体在身体主轴左右摇摆 2° 了。上边的代码生成了头的圆心坐标,第一节身体的四个顶角以及身体两侧的贝塞尔曲线控制点,通过这几个点,就可以画出鱼的头和第一节身体了,并且可以根据动画控制器的数值左右摆动身体。
第二节第三节身体思想和第一节身体一致,不过腰线没有用贝塞尔曲线,而是直接用直线代替,所以二三节身体是梯形,需要注意的是在计算第二三节身体角度的时候摆动核心方法要正余弦相互交替,否则就顺拐了。
鱼鳍的画法也不难,麻烦的地方在于要判断鱼鳍是左边的还是右边的,因为鱼鳍的弧线是贝塞尔曲线生成的,而曲线的控制点要分左右。其中 fatherAngle 是鱼身主轴方向和 X轴 的的夹角,finsAngle 是鱼鳍向内摆动时的偏移角度。
鱼尾是大小两个等腰三角形叠加而成的,三角形的顶点重合。绘制原理是根据三角形底边中点来确定底边的两个点,其中角度和鱼尾主方向垂直。其中newWith
变量的是根据当前动画的过程值动态生成的。
接下来就是激动人心的引擎“发动”时间了,此篇的引擎是一个 ValueAnimator 。 动画周期180秒,数值变化从0到54000,无限循环往复运行,将过程值赋值给 currentValue 然后刷新 Drawable。
运行结果:
动画的分析和实现是一个枯燥又费脑筋的过程,时不时还要复习一下还给老师的数学知识,不过当引擎发动的时候看到绘制的东西动起来了你会觉得所有的努力都是值得的。下一篇将分析如何让鱼儿游动起来,希望大家继续关注。
绘制部分源码:https://github.com/Jichensheng/Fish
推荐阅读:
Android 开发,跳不过的内存管理
阅后即焚?就说截屏你怕不怕?
XxxSdkVersion 傻傻分不清楚
Android 保持屏幕常亮的几种方法
Android 应用内通信 - LocalBroadcast