作者简介
本片是 Jics 的投稿,分享如何分解一个动画效果,来快速实现它。本篇接着上篇的接下去分析,让红鲤鱼游起来。
Jics 的博客地址:
http://www.jianshu.com/p/54f78c38a0f0
前言
上篇文章自定义Drawable实现灵动的红鲤鱼动画(上篇)我们绘制了可以摆动身体的小鱼,本篇就分享一下如何让小鱼游到手指点击的位置。
用到的主要技术如下:
三阶贝塞尔曲线。
Path 的 Measure。
一、动画分析
小鱼的行走不是简单的位移,不难看出在小鱼位移的同时身体的角度还随着前进的方向而变化。
所以本篇要解决如下两部分:
鱼身的位移。
鱼身的旋转。
点击处的水波纹。
二、技术分析
1)鱼身的位移
上篇介绍自定义 Drawable 的时候分析了 Drawable 需要作为 ImageView 的 Drawable 资源或者作为 View 的 background 才可以显示出来,那么我们就可以通过ImageView.setImageDrawable()
将自定义 Drawable 和 ImageView 关联起来,通过位移 ImageView 来移动小鱼。
为了让鱼游动的轨迹更真实,位移路径只有直线是不行的,在鱼需要转身的时候行走路线应该是有弧度的曲线 ,只要涉及到曲线就少不了贝塞尔曲线,涉及到贝塞尔曲线就要涉及到贝塞尔曲线控制点的确定,这里重点介绍一下控制点的确定问题。
上图对关键点都做了简单标注,控制点确定过程如下:
1) 利用头部圆心、鱼身的重心以及点击点坐标来唯一确定一个特征三角形。
2) 确定鱼身需要向左还是向右转弯,这是个很关键的问题。
我们知道,对于同一目的地,向右转和向右转动都可以到达,但是一定有一个最优的方案,假设我们的小鱼有鱼类智商,那么能转 45° 能到达就肯定不会转 315° ,结合这个理论和 1)的特征三角形,可以知道三角形内角AOB就是我们要的转动的角度,知道转动的角度那么转动方向自然而然就知道了。现在我们只有 AOB 三点的坐标如何求出夹角呢?
我们可以 利用向量的夹角公式计算夹角cosAOB = (OA*OB)/(|OA|*|OB|)
其中OA*OB
是向量的数量积,计算过程如下 :
OA=(Ax-Ox,Ay-Oy)
OB=(Bx-Ox,By-Oy)
OAOB=(Ax-Ox)(Bx-Ox)+(Ay-Oy)*(By-Oy)
|OA|表示线段 OA 的模即 OA 的长度,如果对向量不太了解的朋友请自行百度一下。
3) 知道了向左转还是向右转就可以确定曲线的控制点,上图控制点是我凭经验和多次实践确定的比较好的方案第一个控制点就是头部的圆心处,第二个控制点就是转动方向的 1/2 上的一点。
好了,上述的控制点确定之后就可以实用A点、A点、C点、M点来确定一条三阶贝塞尔曲线了。
4) 那么问题来了我们拿到贝塞尔曲线如何让 ImageView 移动呢?
我们经常看到各大直播平台送主播礼物时那些小礼物不规则地向上升是怎么实现呢?原理都差不多,无非就是让控件跟着路径走。传统的做法是利用自定义估值器来计算出动画行走路径,还有一种方法可以不用自定义估值器,Lollipop 版本出来之后属性动画里新增了一个路径动画,我们只用丢进去一个控件和一条路径以及模板参数就能让控件跟着这个路径走。
方法如下:
ObjectAnimator animator = ObjectAnimator
.ofFloat(ivFish, "x", "y", path);
需要明确一点,这里的位移只是平移,也就是说鱼的角度不会因为控件转动而改变,要想让鱼在转弯的时候沿路径切线方向转动请听我继续分析。
2)鱼身的旋转
计算鱼身的旋转角度只用计算出路径切线方向即可,数学里的切线和导数是挂钩的,初代版本我是通过自定义估值器来确定路径的,自定义估值器的时候可以求出当前时刻三阶贝塞尔曲线的导数,那是一个痛苦的过程,公式代码写了十几行,而且效果不好。后来发现一个强大的类PathMeasure
,我们可以通过
getLength()
计算出一条Path的总长度,还可以通过
getPosTan(float distance, float pos[], float tan[])
根据传入的长度计算出路径的某点坐标和切线方向,简直就是为我们量身定做的。其中参数 distance 就是我们需要计算切线的点距Path的起点的距离,通过在AnimatorUpdateListener
中获取 Animator 的当前进度,再与路径总长度相乘,就得到了当前动画已行走的路径长度 distance ,接下来传入两个长度>=2的非空数组 pos 和 tan 数据就可以得到坐标和切线角度的相关参数了。
pos 数组的前两个值就是 (x,y) 的坐标值 tan 前两个值就是所求角的对边和临边的相对长度值(也有可能是绝对长度,因为无法看到 native 源码,但是不管是相对的还是绝对的这两个值的比例知道就可以求出对应的角度了)。
3)点击处的水波纹
水波纹效果比较简单,只需改变圆环的大小和透明度即可,代码部分会详细说明。
分析完位移和旋转,做一个效果图看看大家就更清楚了。为了让大家更清晰地看出效果我把 ImageView 背景设置成蓝色,可以看出蓝色的 ImageView 只负责平移并没有旋转,旋转效果是Drawable中的小鱼执行的。
三、代码实现
文章只贴出主要代码,完整代码文末提供链接。
1、最重要的特征三角夹角计算代码
注意点:
1)变量 abc 是向量 ab 和 ac 的数量积 。
2)angleCos 是弧度值表示的,真正的角度需要通过 Math.toDegrees 转换成角度制。
2、三阶贝塞尔曲线生成代码
其中:
1)fishMiddle 是确定鱼身重心。
2)fishHead获取鱼头圆心 。
3)angle即通过夹角计算方法计算出特征三角形的夹角。
4)delta是鱼身的角度,angle/2+delta就可以得出特征三角形夹角中线跟x轴正方向的角度了。
有了起点 fishMiddle ,转动的长度 1.6R 以及转动的角度(angle/2+delta)就可以通过(上篇)的 calculatPoint()
方法计算出控制点的坐标了,有了控制点就可以通过cubicTo
函数得到三阶贝塞尔曲线了。
3、鱼身转动代码
其中:
1)tan数组变量就是我们存取正切值的两个边的信息数组,通过public static native double atan2(double y, double x);
得到切角的弧度值,转换为角度即可算出转动角度。细心的朋友发现Math.atan2(-tan[1], tan[0])
中的 y值 前边有一个负号 “-” ,这是为了适配 Android 坐标 Y 的正方向和自然直角左边系 Y 轴方向相反的情况。
2)因为我们用不到坐标点信息所以在getPosTan(float distance, float pos[], float tan[])
中传入的 pos 数组是 null 。
3)在动画监听回调中获取到实时角度。
angle = (float) (Math.toDegrees(Math.atan2(-tan[1], tan[0])))
4、水波纹代码
代码比较简单,需要注意的是 ofFloat 中的“radius”
关键字,我们知道默认的属性动画关键字有"alpha"
、"scaleX"
、"scaleY"
、"rotationX"
、"rotationY"
、"Y"
等等,唯独没有“radius”
关键字,对的我们自己定义的,ObjectAnimator 的ofFloat(Object target, String propertyName, float... values)
方法会通过反射技术在参数 target 中寻找关键字对应的 set 方法,即我们需要在“this”类中定义一个setRadius(参数)
方法,其中的参数是我们定义的浮点数0~1中的过程值,通过 setRadius()
方法改变水波纹的 alpha 和半径值,形成水波纹扩散和渐隐的效果。
最后需要注意一点如上代码都是写在一个继承了 RelativeLayout 的自定义 ViewGroup 中, ViewGroup 中 onDraw() 的触发和 View中 不一样,需要在绘制前写上一句setWillNotDraw(false)
来打开强制绘制功能,否则水波纹无法显示。
四、结语
(上篇)得到了很多朋友的支持,非常感动,谢谢大家给予我的鼓励。 动画是个很灵活的事情其实大家可以找找不同的思路来实现,本篇小鱼的转动并不完美,但是我还没找到更好的转弯方法,希望有有更好思路的朋友多多交流。
Github 地址:https://github.com/Jichensheng/Fish_2
推荐阅读: