Jics | 作者

承香墨影 | 校对

http://www.jianshu.com/p/3dd3d1524851 | 原文

Hi,大家好,这里是承香墨影。

今天带来 Drawable 实现红鲤鱼动画的下篇,上篇中,我们绘制了可以摆动身体的鲤鱼。

本篇继续分享,如何让鲤鱼,随我们手指的点击而游动。

最终实现效果如下:

本文涉及到的主要技术如下:

  1. 三阶贝塞尔曲线;

  2. Path 的 Measure;

一、动画分析

小鱼的行走不是简单的位移,不难看出在小鱼位移的同时,身体的角度,还随着前进的方向而变化,所以本篇要解决如下 3 部分:

  1. 鱼身的位移;

  2. 鱼身的旋转;

  3. 点击处的水波纹;

二、技术分析

2.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)OA*OB=(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.2 鱼身的旋转

计算鱼身的旋转角度,只用计算出路径切线方向即可。

数学里的切线和导数是挂钩的,初代版本我是通过自定义估值器,来确定路径的。自定义估值器的时候,可以求出当前时刻三阶贝塞尔曲线的导数。

那是一个痛苦的过程,公式代码写了十几行,而且效果不好。后来发现一个强大的类 PathMeasure,我们可以通过 getLength()计算出一条 Path 的总长度,还可以通过 getPosTan(float distance, float pos[], float tan[]),根据传入的长度,计算出路径的某点坐标和切线方向。简直就是为我们量身定做的。

其中参数 distance,就是我们需要计算切线的点距 Path 的起点的距离,通过在 AnimatorUpdateListener 中获取 Animator 的当前进度,再与路径总长度相乘,就得到了当前动画,已行走的路径长度 distance。

接下来传入两个长度 >=2 的非空数组 pos 和 tan 数据,就可以得到坐标和切线角度的相关参数了。

pos 数组的前两个值就是 x,y 的坐标值,tan 前两个值,就是所求角的对边和临边的相对长度值(也有可能是绝对长度,因为无法看到 native 源码,但是,不管是相对的还是绝对的这两个值的比例,知道了就可以求出对应的角度)。

2.3 点击处的水波纹

水波纹效果比较简单,只需改变圆环的大小和透明度即可,代码部分会详细说明。

分析完位移旋转,做一个效果图,看看大家就更清楚了。

为了让大家更清晰地看出效果我把 ImageView 背景设置成蓝色,可以看出蓝色的 ImageView 只负责平移并没有旋转,旋转效果是 Drawable 中的小鱼执行的。

三、代码实现

文章只贴出主要代码,完整代码文末提供链接

3.1 最重要的特征三角夹角计算代码

注意点:

  1. 变量 abc 是向量 ab 和 ac 的数量积;

  2. angleCos 是弧度值表示的,真正的角度,需要通过 Math.toDegrees() 转换成角度制;

 /*** 利用向量的夹角公式计算夹角* cosBAC = (AB*AC)/(|AB|*|AC|)* 其中AB*AC是向量的数量积AB=(Bx-Ax,By-Ay)  AC=(Cx-Ax,Cy-Ay),AB*AC=(Bx-Ax)*(Cx-Ax)+(By-Ay)*(Cy-Ay)** @param center 顶点 A* @param head   点1  B* @param touch  点2  C* @return*/
public static float includedAngle(PointF center, PointF head, PointF touch) {float abc = (head.x - center.x) * (touch.x - center.x) + (head.y - center.y) * (touch.y - center.y);float angleCos = (float) (abc /((Math.sqrt((head.x - center.x) * (head.x - center.x) + (head.y - center.y) * (head.y - center.y)))* (Math.sqrt((touch.x - center.x) * (touch.x - center.x) + (touch.y - center.y) * (touch.y - center.y)))));float temAngle = (float) Math.toDegrees(Math.acos(angleCos));//判断方向  正:左侧  负:右侧 0:线上,但是Android的坐标系Y是朝下的,所以左右颠倒一下float direction = (center.x - touch.x) * (head.y - touch.y) - (center.y - touch.y) * (head.x - touch.x);//线上还要判断是同向还是逆向if (direction == 0) {if (abc >= 0) {return 0;} elsereturn 180;} else {if (direction > 0) {//右侧顺时针为负return -temAngle;} else {return temAngle;}}
}

3.2 三阶贝塞尔曲线生成代码

其中:

  1. fishMiddle 是确定鱼身重心;

  2. fishHead 获取鱼头圆心 ;

  3. angle 即通过夹角计算方法计算出特征三角形的夹角;

  4. delta 是鱼身的角度,angle/2+delta 就可以得出特征三角形夹角中线跟 x 轴正方向的角度了;

有了起点 fishMiddle ,转动的长度 1.6R 以及转动的角度(angle/2+delta)就可以通过(上篇)的 calculatPoint() 方法计算出控制点的坐标了,有了控制点就可以通过 cubicTo() 函数得到三阶贝塞尔曲线了。

Path path = new Path();
PointF fishMiddle = new PointF(ivFish.getX() + fishDrawable.getMiddlePoint().x, ivFish.getY() + fishDrawable.getMiddlePoint().y);
PointF fishHead = new PointF(ivFish.getX() + fishDrawable.getHeadPoint().x, ivFish.getY() + fishDrawable.getHeadPoint().y);
path.moveTo(ivFish.getX(), ivFish.getY());final float angle = includedAngle(fishMiddle, fishHead, touch);
float delta = calcultatAngle(fishMiddle, fishHead);PointF controlF = calculatPoint(fishMiddle, 1.6f*fishDrawable.HEAD_RADIUS, angle / 2 + delta);
path.cubicTo(fishHead.x, fishHead.y, controlF.x, controlF.y, touch.x - fishDrawable.getHeadPoint().x, touch.y - fishDrawable.getHeadPoint().y);

3.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])))

final float[] tan = new float[2];
//设置为false代表不强制把Path闭合
final PathMeasure pathMeasure = new PathMeasure(path, false);animator = ObjectAnimator.ofFloat(ivFish, "x", "y", path);
animator.setDuration(2 * 1000);
animator.setInterpolator(new AccelerateDecelerateInterpolator());animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float fraction = animation.getAnimatedFraction();pathMeasure.getPosTan(pathMeasure.getLength() * fraction, null, tan);float angle = (float) (Math.toDegrees(Math.atan2(-tan[1], tan[0])));fishDrawable.setMainAngle(angle);}
});

4、水波纹代码

代码比较简单,需要注意的是 ofFloat 中的 radius 关键字。

我们知道,默认的属性动画关键字有 alphascaleXscaleYrotationXrotationYY 等等,唯独没有 radius 关键字。

对的我们自己定义的 ObjectAnimator 的 ofFloat(Object target, String propertyName, float... values) 方法,会通过反射,在参数 target 中寻找关键字对应的 set 方法,即我们需要在 this 类中定义一个 setRadius(参数) 方法,其中的参数是我们定义的浮点数 0~1 中的过程值,通过 setRadius() 方法,改变水波纹的 alpha 和半径值,形成水波纹扩散和渐隐的效果

rippleAnimator = ObjectAnimator.ofFloat(this, "radius", 0f, 1f).setDuration(1000);public void setRadius(float currentValue) {alpha = (int) (100 * (1 - currentValue) / 2);radius = DEFAULT_RADIUS * currentValue;invalidate();
}

最后需要注意一点。如上代码都是写在一个继承了 RelativeLayout 的自定义 ViewGroup 中, ViewGroup 中 onDraw() 的触发和 View 中不一样。

需要在绘制前写上一句 setWillNotDraw(false) 来打开强制绘制功能,否则水波纹无法显示。

四、结语

上篇自定义 Drawable 实现灵动的红鲤鱼动画(上篇)得到了很多朋友的支持,非常感动,谢谢大家给予我的鼓励。

动画是个很灵活的事情其实大家可以找找不同的思路来实现,本篇小鱼的转动并不完美,但是我还没找到更好的转弯方法,希望有有更好思路的朋友多多交流。

github 地址:https://github.com/Jichensheng/Fish_2

-- End --

本文对你有帮助吗?留言、转发、点好看是最大的支持,谢谢!

推荐阅读:

微信Matrix不好用?主要原因是监控日志解析方面 - 那手写一个!

效果炸了,自定义Drawable实现灵动的红鲤鱼动画(上)

HTTP/2.0 原理!与 1.x 相比,到底优化了什么?

效果炸了,Drawable 实现红鲤鱼动画,点哪儿游哪儿(下)相关推荐

  1. android开发 鱼动画,自定义Drawable实现灵动的红鲤鱼动画(上篇)

    此篇中的小鱼动画是模仿国外一个大牛做的flash动画,第一眼就爱上它了,简约灵动又不失美学,于是抽空试着尝试了一下,如下是我用Android实现的效果图: 小鱼儿 由于整个绘制分析过程比较繁琐所以灵动 ...

  2. [转]自定义Drawable实现灵动的红鲤鱼动画(上篇)

    此篇中的小鱼动画是模仿国外一个大牛做的flash动画,第一眼就爱上它了,简约灵动又不失美学,于是抽空试着尝试了一下,如下是我用Android实现的效果图: 小鱼儿 由于整个绘制分析过程比较繁琐所以灵动 ...

  3. 自定义Drawable实现灵动的红鲤鱼动画(下篇)

    上篇文章自定义Drawable实现灵动的红鲤鱼动画(上篇)我们绘制了可以摆动身体的小鱼,本篇就分享一下如何让小鱼游到手指点击的位置.用到的主要技术如下: 1).三阶贝塞尔曲线 2).Path的Meas ...

  4. [转]自定义Drawable实现灵动的红鲤鱼动画(下篇)

    小鱼儿 上篇文章自定义Drawable实现灵动的红鲤鱼动画(上篇)我们绘制了可以摆动身体的小鱼,本篇就分享一下如何让小鱼游到手指点击的位置.用到的主要技术如下: 1).三阶贝塞尔曲线 2).Path的 ...

  5. 效果炸了,自定义Drawable实现灵动的红鲤鱼动画(上)

    Jics | 作者 承香墨影 | 校对 http://www.jianshu.com/p/3dd3d1524851 | 原文 一.前言 Hi,大家好,这里是承香墨影! 此篇中的小鱼动画是模仿国外一个大 ...

  6. android 动画之漂移,Android之自定义Drawable实现灵动的红鲤鱼动画(上篇)

    此篇中的小鱼动画是模仿国外一个大牛做的flash动画,第一眼就爱上它了,简约灵动又不失美学,于是抽空试着尝试了一下,如下是我用Android实现的效果图: 由于整个绘制分析过程比较繁琐所以灵动的红鲤鱼 ...

  7. 自定义Drawable实现灵动的红鲤鱼动画(上篇)

    此篇中的小鱼动画是模仿国外一个大牛做的flash动画,第一眼就爱上它了,简约灵动又不失美学,于是抽空试着尝试了一下,如下是我用Android实现的效果图: 由于整个绘制分析过程比较繁琐所以灵动的红鲤鱼 ...

  8. 自定义 Drawable实现灵动红鲤鱼特效

    1 前言 Hi,大家好,这里是承香墨影! 此篇中的小鱼动画是模仿国外一个大牛做的 Flash 动画,第一眼就爱上它了,简约灵动又不失美学,于是抽空试着尝试了一下,如下是我用 Android 实现的效果 ...

  9. android如何使用gif动画效果,Android中用GifView显示Gif动画及Gifview简介

    最近项目中要用到是实现Gif格式的动画,查了查Android压根就不支持Gif动画,于是乎就研究下了开源的"Gifview". 一.Gifview简介 作者:ant.cy.liao ...

最新文章

  1. 子程序调用与宏定义的异同_如何用数控系统进行简单的宏程序调用?老师傅告诉你,用G65就行...
  2. python文件管理_超值的Python文件操作与管理!
  3. 04. Web大前端时代之:HTML5+CSS3入门系列~HTML5 表单
  4. 收藏 | 北大华为鹏城联合首次提出视觉 Transformer 后量化算法!
  5. github 开放_GitHub为女性开发人员所做的工作,Tim O'Reilly谈开放数据等
  6. 爬虫项目三:爬取选课信息
  7. OpenShift 4 - DevSecOps Workshop (5) - 为Pipeline增加测试Task
  8. python性能解决的事_Python程序的性能分析方法
  9. Burp Suite Scanner Module - 扫描模块
  10. JAVA项目中遇到URLEncoder URLDecoder编码解码问题
  11. Linux中hosts文件的修改
  12. PDA开发从入门到精通
  13. python脚本打包成exe+配置文件
  14. 惊喜来袭~进阶版《看漫画学Python 2:有趣、有料、好玩、好用》
  15. linux主机如何安装杀毒软件,Linux 杀毒软件ClamAV安装部署
  16. 电子学会 2020年6月 青少年软件编程Python编程等级考试一级真题解析(选择题+判断题+编程题)
  17. 吴恩达机器学习ex2 Logistic Regression (python)
  18. ps基础入门2-图层样式
  19. 使用BIND9+MySql搭建一个智能解析DNS
  20. 拨乱反正:DDD 回归具体的业务场景,Domain Model 再再重新设计

热门文章

  1. wework oracle,氪空间总裁钟澍:中国联合办公已经不需要再去效仿WeWork
  2. 高新企业认定后的条件
  3. Excel学习日记:L21-表格数值格式
  4. 创业起步阶段需要注意的几点
  5. 计算机审计试题及答案,计算机审计练习题及答案
  6. 计算机专业要学英语口语,学习英语口语必须掌握两大法宝
  7. linux7 ppt,Linux_7_.ppt
  8. UVa1646 - Edge Case
  9. word 模板的位置
  10. 钉钉小程序的开发入门【亲自调试】