Jics | 作者

承香墨影 | 校对

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

一、前言

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

此篇中的小鱼动画是模仿国外一个大牛做的 Flash 动画,第一眼就爱上它了,简约灵动又不失美学,于是抽空试着尝试了一下,如下是我用 Android 实现的效果图:

由于整个绘制分析过程比较繁琐,所以灵动的红鲤鱼准备做成上下两篇,本篇是小鱼儿绘制的实现篇,第二篇是小鱼儿游动控制篇。

本篇实现如下效果:

绘制实现篇用到如下主要的技术:

  1. 自定义 Drawable 动画;

  2. Android 的坐标及角度;

  3. Canvas 中 Layer 的使用;

  4. 正余弦函数的使用以及角度角和弧度角的转换;

下图是我实现小鱼儿的分解图纸:

二、动画拆解

拿到动画需求或者模仿一个动画,首先需要分析动画主体,如何绘制部件如何活动,就此动画外观分析如下:

  1. 小鱼的身体各个部件都是简单的半透明几何图形;

  2. 各个部件都可以活动;

  3. 从头到尾方向的部件摆动幅度越来越大、频率越来越高;

三、技术分析

小鱼摆动是周期运动,三角函数正好有此特性,角度问题也需要和坐标挂钩,所以我们先来明确一下两个最重要也是最基本的问题:坐标和角度

与平面直角坐标系不同的是 Android 的坐标系中 Y 轴正方向是朝下的 ,但是角度却和平面直角坐标系的计算方法一样,即 原点指向 X 轴正方向为 0°,正角度是逆时针旋转,负角度是顺时针旋转

那么问题就来了:坐标系不同,角度转动方式却一样,为了让 Java 中的 Math 函数计算出来的角度跟 Android 的坐标习惯一致,我们需要将与 Y 轴相关的角度都减去 180°,这样解决了既用 Android 的坐标又用自然角度的问题,即下图所示的角度和坐标系关系。

统一完角度问题,接下来我们就看看鱼的各部件,是怎么关联在一起的?

需要先了解三个重要参数。

3.1 鱼的重心

因为最终我们要实现鱼儿,根据手指点击的位置,而移动的效果,必须确保能让点击点,成为唯一确定鱼儿位置的点,所以我们必须找到一个让鱼儿的各个部件,都相对此点绘制的点。

参考点可以任意选,但是考虑到转弯的时候,或者身体摆动的时候不会往某一边偏,于是将参考点选在鱼的中轴线上,本来选在中轴线和鱼儿头顶橡胶的点但是最后转弯的时候就跟秋名山老司机漂移一样,那叫一个飘逸,最后将参考点选在了鱼的腹部重心处。

3.2 鱼的半径

此案例中鱼的各个部件都是以 鱼头半径 R 为单位 衡量的,比如鱼的身子第一节长度是 3.2R,依次确定好身体的各个部件,相对于鱼头半径的尺寸就能确定整条鱼的总长度为 6.79R,继而确定控件的总尺寸。

如下图,经过计算控件最小尺寸为 8.36R,这样就保证鱼儿转动任意角度都在控件之内。

3、鱼身角度

此处的鱼身角度,是指重心到鱼头圆心的连线和 X 轴正方向的夹角角度,即鱼儿前进方向的角度。此方向是确定各个部件方向及位置的的基础方向,部件的定位、鱼身角度,以及尾部的摆动角度,都是在此角度基础上,通过加减角度来控制左右摇摆。

下边我将演示一下,如何通过这三个因素来确定头部以及鱼鳍的点坐标(其他部位原理相同)。

先假设鱼身角度为 0°,即头朝向 X 轴正方向。通过重心点以及第一节身长的一半的长度,以及角度即可计算出头部的圆心坐标,然后再以头部圆心坐标和 0.9R 的长度,顺时针旋转 80° 确定右边鱼鳍的坐标点。

鱼鳍绘制原理相似,通过上文的右鳍坐标,可以计算出右鳍的另一端坐标,鱼鳍弧度是通过二阶贝塞尔曲线绘制的。

鱼尾张合分析。鱼尾是内外两个三角形叠加而成的,三角形顶点和三角形底边中点连线的角度,和最后一节身体的角度一直,三角形底边左右两点通过底边的中点,以及动态计算出来的长度确定的。

后用放出骨架系统:黑线为各个部件的主轴,圆圈为各个部件边界的定位点或贝塞尔曲线的控制点,是不是很酷,像不像电影里的动作捕捉。

四、代码实现

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

1、自定义 Drawable

自定义 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。

@Override
public int getIntrinsicHeight() {return (int) (8.38f * HEAD_RADIUS);
}@Override
public int getIntrinsicWidth() {return (int) (8.38f * HEAD_RADIUS);
}

其次自定义 Drawable 只需复写必要的四个函数,比较简单具体作用见注释。

@Override
public void draw(Canvas canvas) {//和自定义View中的onDraw()异曲同工
}@Override
public void setAlpha(int alpha) {//设置Drawable的透明度,一般情况下将此alpha值设置给Paint
}@Override
public void setColorFilter(ColorFilter colorFilter) {//设置颜色滤镜,一般情况下将此值设置给Paint
}@Override
public int getOpacity() {//决定绘制的部分是否遮住Drawable下边的东西,有点抽象,有几种模式//PixelFormat.UNKNOWN//PixelFormat.TRANSLUCENT 只有绘制的地方才盖住下边//PixelFormat.TRANSPARENT 透明,不显示绘制内容//PixelFormat.OPAQUE 完全盖住下边内容return PixelFormat.TRANSLUCENT;
}

主要是复写 draw() 方法,利用 Canvas 绘制各种想要的东西。

2、坐标部分

最最最主要的坐标计算代码,小鱼儿所有部件都是通过此方法计算出坐标的,功能是计算一个点的坐标,可以理解为一个长度为 length 的线绕起点 startPoint 旋转 angle 角度后线段另一端的坐标。

/***  输入起点、长度、旋转角度计算终点* @param startPoint 起点* @param length 长度* @param angle 旋转角度* @return 计算结果点*/
private static PointF calculatPoint(PointF startPoint, float length, float angle) {float deltaX = (float) Math.cos(Math.toRadians(angle)) * length;//符合Android坐标的y轴朝下的标准float deltaY = (float) Math.sin(Math.toRadians(angle-180)) * length;return new PointF(startPoint.x + deltaX, startPoint.y + deltaY);
}

这里要特别说明一下 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 的话就强转成 float 型就行了,可能是由于 double 的精度问题。

3、第一节身体

第一节身体包括头部和身体的第一段,代码如下(虚线部分是身体其他部分的生成方法,暂时不管)。

private void makeBody(Canvas canvas, float headRadius) {float angle = mainAngle + (float) Math.sin(Math.toRadians(currentValue * 1.2 * waveFrequence)) * 2;headPoint = calculatPoint(middlePoint, BODY_LENGHT / 2,mainAngle);//画头canvas.drawCircle(headPoint.x, headPoint.y, HEAD_RADIUS, mPaint);...............PointF point1, point2, point3, point4, contralLeft, contralRight;//point1和4的初始角度决定发髻线的高低值越大越低point1 = calculatPoint(headPoint, headRadius,  angle-80);point2 = calculatPoint(endPoint, headRadius * 0.7f, angle-90);point3 = calculatPoint(endPoint, headRadius * 0.7f, angle +90);point4 = calculatPoint(headPoint, headRadius, angle +80);//决定胖瘦contralLeft = calculatPoint(headPoint, BODY_LENGHT * 0.56f, angle -130);contralRight = calculatPoint(headPoint, BODY_LENGHT * 0.56f, angle +130);mPath.reset();mPath.moveTo(point1.x, point1.y);mPath.quadTo(contralLeft.x, contralLeft.y, point2.x, point2.y);mPath.lineTo(point3.x, point3.y);mPath.quadTo(contralRight.x, contralRight.y, point4.x, point4.y);mPath.lineTo(point1.x, point1.y);mPaint.setColor(Color.argb(BODY_ALPHA, 244, 92, 71));//画身子canvas.drawPath(mPath, mPaint);
}

其中最难理解的是角度的计算这句话:

//中心轴线和X轴顺时针方向夹角
float angle = mainAngle + (float) Math.sin(Math.toRadians(currentValue * 1.2 * waveFrequence)) * 2;

这里 Math.sin(Math.toRadians(currentValue * 1.2 * waveFrequence)) 是控制第一节身体摆动的核心方法,变量 currentValue 是 ValueAnimator 动画的过程数值, 1.2 是用来控制身体摆动的固有频率, waveFrequence 是全局频率,用于控制鱼儿运动时的摆动频率,因为 sin 函数 是周期函数,且值域为 [-1,1] ,计算结果乘 2 之后这句话就可以生成一个 [-2,2] 的变化范围,用这个值加上 mainAngle(身体前进方向和 X 轴正方向夹角)就可以让鱼的第一节身体在身体主轴左右摇摆 2° 了。上边的代码生成了头的圆心坐标,第一节身体的四个顶角以及身体两侧的贝塞尔曲线控制点,通过这几个点,就可以画出鱼的头和第一节身体了,并且可以根据动画控制器的数值左右摆动身体。

第二节第三节身体思想和第一节身体一致,不过腰线没有用贝塞尔曲线,而是直接用直线代替,所以二三节身体是梯形,需要注意的是在计算第二三节身体角度的时候摆动核心方法要正余弦相互交替,否则就顺拐了

4、鱼鳍

鱼鳍的画法也不难,麻烦的地方在于,要判断鱼鳍是左边的还是右边的,因为鱼鳍的弧线是贝塞尔曲线生成的,而曲线的控制点要分左右。其中 fatherAngle 是鱼身主轴方向和 X 轴 的的夹角,finsAngle 是鱼鳍向内摆动时的偏移角度。

private void makeFins(Canvas canvas, PointF startPoint, int type, float fatherAngle) {//鱼鳍控制点相对于鱼主轴方向的角度float contralAngle = 115;mPath.reset();mPath.moveTo(startPoint.x, startPoint.y);//鱼鳍的另一端PointF endPoint = calculatPoint(startPoint, FINS_LENGTH, type == FINS_RIGHT ? fatherAngle - finsAngle-180 : fatherAngle + finsAngle+180);//曲线的控制点PointF contralPoint = calculatPoint(startPoint, FINS_LENGTH * 1.8f, type == FINS_RIGHT ?fatherAngle - contralAngle - finsAngle : fatherAngle + contralAngle + finsAngle);mPath.quadTo(contralPoint.x, contralPoint.y, endPoint.x, endPoint.y);mPath.lineTo(startPoint.x, startPoint.y);mPaint.setColor(Color.argb(FINS_ALPHA, 244, 92, 71));canvas.drawPath(mPath, mPaint);mPaint.setColor(Color.argb(OTHER_ALPHA, 244, 92, 71));
}

5、鱼尾

鱼尾是大小两个等腰三角形叠加而成的,三角形的顶点重合。绘制原理是根据三角形底边中点来确定底边的两个点,其中角度和鱼尾主方向垂直。其中newWith变量的是根据当前动画的过程值动态生成的。

private void makeTail(Canvas canvas, PointF mainPoint, float length, float maxWidth, float angle) {float newWidth = (float) Math.abs(Math.sin(Math.toRadians(currentValue * 1.7 * waveFrequence)) * maxWidth + HEAD_RADIUS/5*3);//endPoint为三角形底边中点PointF endPoint = calculatPoint(mainPoint, length, angle-180);PointF endPoint2 = calculatPoint(mainPoint, length - 10, angle-180);PointF point1, point2, point3, point4;point1 = calculatPoint(endPoint, newWidth, angle-90);point2 = calculatPoint(endPoint, newWidth, angle +90);point3 = calculatPoint(endPoint2, newWidth - 20, angle-90);point4 = calculatPoint(endPoint2, newWidth - 20, angle +90);//内mPath.reset();mPath.moveTo(mainPoint.x, mainPoint.y);mPath.lineTo(point3.x, point3.y);mPath.lineTo(point4.x, point4.y);mPath.lineTo(mainPoint.x, mainPoint.y);canvas.drawPath(mPath, mPaint);//外mPath.reset();mPath.moveTo(mainPoint.x, mainPoint.y);mPath.lineTo(point1.x, point1.y);mPath.lineTo(point2.x, point2.y);mPath.lineTo(mainPoint.x, mainPoint.y);canvas.drawPath(mPath, mPaint);
}

5、动画引擎

接下来就是激动人心的引擎“发动”时间了,此篇的引擎是一个 ValueAnimator。

动画周期 180 秒,数值变化从 0 到 54000,无限循环往复运行,将过程值赋值给 currentValue 然后刷新 Drawable。

//引擎部分
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 54000);
valueAnimator.setDuration(180 * 1000);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {currentValue = (int) (animation.getAnimatedValue());invalidateSelf();}
});

运行结果:

五、结语

动画的分析和实现是一个枯燥又费脑筋的过程,时不时还要复习一下还给老师的数学知识,不过当引擎发动的时候看到绘制的东西动起来了你会觉得所有的努力都是值得的。下一篇将分析如何让鱼儿游动起来,希望大家继续关注。

绘制部分源码:https://github.com/Jichensheng/Fish

-- End --

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

推荐阅读:

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

安卓原生运行Win11 再跑 Apk,搁着套娃呢!

try-catch OOM,可行吗?

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

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

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

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

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

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

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

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

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

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

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

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

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

  7. 效果炸了,Drawable 实现红鲤鱼动画,点哪儿游哪儿(下)

    Jics | 作者 承香墨影 | 校对 http://www.jianshu.com/p/3dd3d1524851 | 原文 Hi,大家好,这里是承香墨影. 今天带来 Drawable 实现红鲤鱼动画 ...

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

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

  9. android 自定义view 动画效果,Android自定义view实现阻尼效果的加载动画

    效果: 需要知识: 1. 二次贝塞尔曲线 2. 动画知识 3. 基础自定义view知识 先来解释下什么叫阻尼运动 阻尼振动是指,由于振动系统受到摩擦和介质阻力或其他能耗而使振幅随时间逐渐衰减的振动,又 ...

最新文章

  1. WPF DataGrid 绑定DataSet数据 自动生成行号
  2. 与自定义词典 分词_如何掌握分词技术,你需要学会这些
  3. 站长圈转风向标了 都玩自媒体了!
  4. 左滑右滑,在VS Code里滑个妹纸给你写喜欢的代码?
  5. 【Groovy】集合遍历 ( 使用集合的 find 方法查找集合元素 | 闭包中使用 == 作为查找匹配条件 | 闭包中使用 is 作为查找匹配条件 | 闭包使用 true 作为条件 | 代码示例 )
  6. Django框架连接MySQL数据库
  7. python3 实现 A+B Problem(百练OJ:1000)
  8. 弹出框页面中使用jquery.validate验证控件
  9. maven项目添加jar包
  10. 计算机右键管理显示没权限,解决右键 选择打开方式提示没有权限
  11. django+nginx+uwsgi部署web站点
  12. udl 连mysql_自己如何正确获取MYSQL的ADO连接字符串
  13. 信息学奥赛一本通 1148:连续出现的字符 | OpenJudge NOI 1.9 11
  14. python中模块下载方法(conda+pip)
  15. ArcGIS Pro 3.0最新消息
  16. 电阻式触摸屏和电容式触摸屏区别
  17. 为什么宁愿工资低点,也不建议去外包公司?
  18. 第七课:BootRom的烧录
  19. 正则表达式学习——(2)正则回溯
  20. KZ笔记2:视角控制

热门文章

  1. 河海大学计算机专硕考研万字经验贴
  2. 贴吧怎么给公众号引流?利用贴吧引流到微信公众号
  3. solidworks卸载
  4. ajax接口500错误,djangoajax请求返回500个内部服务器错误
  5. 电脑软件:国内最好用解压缩软件 7-Zip 新版本发布
  6. layui table 头部工具栏右侧图标隐藏,增加
  7. 被脱裤也不怕,密码安全可以这样保障
  8. Qt笔记(二十)之实现窗口定时关闭
  9. VMware Workstation 下载和安装
  10. 【python版CV】- 银行卡号识别项目