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

 
小鱼儿

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

 
原地摆尾版

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

1)、自定义Drawable动画
2)、Android的坐标及角度
3)、Canvas中layer的使用
4)、正余弦函数的使用以及角度角和弧度角的转换

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

 
部件分解图

一、动画拆解

拿到动画需求或者模仿一个动画首先需要分析动画主体如何绘制部件如何活动,就此动画外观分析如下:
1)、小鱼的身体各个部件都是简单的半透明几何图形
2)、各个部件都可以活动
3)、从头到尾方向的部件摆动幅度越来越大、频率越来越高

二、技术分析

小鱼摆动是周期运动,三角函数正好有此特性,角度问题也需要和坐标挂钩,所以我们先来明确一下两个最重要也是最基本的问题:坐标和角度。与平面直角坐标系不同的是Android的坐标系中Y轴正方向是朝下的,但是角度却和平面直角坐标系的计算方法一样,即原点指向X轴正方向为0°,正角度是逆时针旋转,负角度是顺时针旋转那么问题就来了:坐标系不同,角度转动方式却一样,为了让java中的Math函数计算出来的角度跟Android的坐标习惯一致我们需要将与Y轴相关的角度都减去180°,这样解决了既用Android的坐标又用自然角度的问题,即下图所示的角度和坐标系关系
  

 
Android坐标系下的自然角度

  
  统一完角度问题,接下来我们就看看鱼的各部件是怎么关联在一起的。需要先了解三个重要参数

1)、鱼的重心

因为最终我们要实现鱼儿根据手指点击的位置而移动的效果,必须确保能让点击点成为唯一确定鱼儿位置的点,所以我们必须找到一个让鱼儿的各个部件都相对此点绘制的点。参考点可以任意选,但是考虑到转弯的时候或者身体摆动的时候不会往某一边偏,于是将参考点选在鱼的中轴线上,本来选在中轴线和鱼儿头顶橡胶的点但是最后转弯的时候就跟秋名山老司机漂移一样,那叫一个飘逸,最后将参考点选在了鱼的腹部重心处。

2)、鱼头半径

 
比例示意图

此案例中鱼的各个部件都是以鱼头半径R为单位衡量的,比如鱼的身子第一节长度是3.2R,依次确定好身体的各个部件相对于鱼头半径的尺寸就能确定整条鱼的总长度为6.79R,继而确定控件的总尺寸。如下图,经过计算控件最小尺寸为8.36R,这样就保证鱼儿转动任意角度都在控件之内

 
打转图

3)、鱼身角度

此处的鱼身角度是指重心到鱼头圆心的连线和X轴正方向的夹角角度,即鱼儿前进方向的角度。此方向是确定各个部件方向及位置的的基础方向,部件的定位、鱼身角度以及尾部的摆动角度都是在此角度基础上通过加减角度来控制左右摇摆。
 下边我将演示一下如何通过这三个因素来确定头部以及鱼鳍的点坐标(其他部位原理相同)
 先假设鱼身角度为0°,即头朝向X轴正方向。通过重心点以及第一节身长的一半的长度,以及角度即可计算出头部的圆心坐标,然后再以头部圆心坐标和0.9R的长度,顺时针旋转80°确定右边鱼鳍的坐标点
 

 
鱼鳍定位过程

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

鱼尾张合分析。鱼尾是内外两个三角形叠加而成的,三角形顶点和三角形底边中点连线的角度和最后一节身体的角度一直,三角形底边左右两点通过底边的中点以及动态计算出来的长度确定的
    
  最后用放出骨架系统:黑线为各个部件的主轴,圆圈为各个部件边界的定位点或贝塞尔曲线的控制点,是不是很酷,像不像电影里的动作捕捉
  

 
骨架系统

三、代码实现

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

0)自定义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绘制各种想要的东西。

1)坐标部分

最最最主要的坐标计算代码,小鱼儿所有部件都是通过此方法计算出坐标的 ,功能是计算一个点的坐标,可以理解为一个长度为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的精度问题。

2)、第一节身体

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

 
头身
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);
}

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

    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°了。上边的代码生成了头的圆心坐标,第一节身体的四个顶角以及身体两侧的贝塞尔曲线控制点,通过这几个点,就可以画出鱼的头和第一节身体了,并且可以根据动画控制器的数值左右摆动身体

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

3)、鱼鳍

鱼鳍的画法也不难,麻烦的地方在于要判断鱼鳍是左边的还是右边的,因为鱼鳍的弧线是贝塞尔曲线生成的,而曲线的控制点要分左右。其中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)); }
 
鱼鳍定位过程

4)、鱼尾

鱼尾是大小两个等腰三角形叠加而成的,三角形的顶点重合。绘制原理是根据三角形底边中点来确定底边的两个点,其中角度和鱼尾主方向垂直。其中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)、动画引擎

接下来就是激动人心的引擎“发动”时间了,看过上篇文章Android仿百度贴吧客户端Loading小球的朋友就知道引擎部分是一个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() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentValue = (int) (animation.getAnimatedValue());
invalidateSelf();
}
});

运行结果:

 
感谢女朋友的默默支持

四、结语

动画的分析和实现是一个枯燥又费脑筋的过程,时不时还要复习一下还给老师的数学知识,不过当引擎发动的时候看到绘制的东西动起来了你会觉得所有的努力都是值得的。下一篇将分析如何让鱼儿游动起来,希望大家继续关注。
绘制部分源码:灵动的红鲤鱼Github源码
CSDN同步分析文章链接: 自定义Drawable实现灵动的红鲤鱼动画(上篇)
下篇链接: 自定义Drawable实现灵动的红鲤鱼动画(下篇)

作者:Jics
链接:https://www.jianshu.com/p/3dd3d1524851
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

[转]自定义Drawable实现灵动的红鲤鱼动画(上篇)的更多相关文章

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

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

  2. Canvas 实现灵动的红鲤鱼动画(上)

    前言 上一篇文章<Canvas 仿百度贴吧客户端 loading 小球>实现了百度贴吧客户端的 loading 小球效果,同时还留下了一个任务:实现灵动的红鲤鱼动画. 这个动画效果实现起来 ...

  3. 自定义Drawable

    本文由 伯乐在线 - treesouth 翻译,toolate 校稿.未经许可,禁止转载! 英文出处:ryanharter.com.欢迎加入翻译小组. 我们看过一些博客文章,讲述了为什么要适时地使用自 ...

  4. P4773 红鲤鱼与绿鲤鱼

    P4773 红鲤鱼与绿鲤鱼 暑假比赛的一个水题 总情况数:\(\dfrac{(a+b)!}{a!b!}\) 就是\(a+b\)条鲤鱼中选\(a\) or \(b\)的情况 反正我们会用完鲤鱼,则红鲤鱼 ...

  5. Android APK开发 Drawable文件夹下的自定义Drawable文件

    版本:2018/2/11 Drawable的分类 自定义Drawable SVG矢量图 个人总结的知识点外,部分知识点选自<Android开发艺术探索>-第六章 Drawable 1.Dr ...

  6. Android特效专辑(五)——自定义圆形头像和仿MIUI卸载动画—粒子爆炸

    Android特效专辑(五)--自定义圆形头像和仿MIUI卸载动画-粒子爆炸 好的,各位亲爱的朋友,今天讲的特效还是比较炫的,首先,我们会讲一个自定义圆形的imageView,接着,我们会来实现粒子爆 ...

  7. Android自定义View 画弧形,文字,并增加动画效果

    一个简单的Android自定义View的demo,画弧形,文字,开启一个多线程更新ui界面,在子线程更新ui是不允许的,但是View提供了方法,让我们来了解下吧. 1.封装一个抽象的View类   B ...

  8. Android 自定义Drawable

    1.使用BitmapShader实现图片圆角 public class CornerDrawable extends Drawable { private Paint mPaint; private ...

  9. Android自定义drawable(Shape)详解

    在Android开发过程中,经常需要改变控件的默认样式, 那么通常会使用多个图片来解决.不过这种方式可能需要多个图片,比如一个按钮,需要点击时的式样图片,默认的式样图片. 这样就容易使apk变大. 那 ...

随机推荐

  1. C#高级编程9-第9章 字符串和正则表达式

    字符串和正则表达式 String类 String类对象是不可改变的,对于String对象的重新赋值在本质上是重新创建了一个String对象并将新值赋予该对象,其方法ToString对性能的提高并非很显 ...

  2. AbstractAction

    package cn.tz.action.abs; import java.io.File; import java.io.IOException; import java.text.SimpleDa ...

  3. scriptlet

    <!-- <%! %>:可以修饰全局变量.常量.类.方法 对应java类中的成员变量.常量.内部类.成员方法 --> <%! int num=10;//全局变量 publ ...

  4. flash 跨域加载 二次加载

    var url2:String = "http://thirdapp0.qlogo.cn/qzopenapp/fa5d80ebf9fc89aaa1d7ddb0e1b861e58d77b409 ...

  5. 三个实例演示 Java Thread Dump 日志分析(转)

    原文链接:http://www.cnblogs.com/zhengyun_ustc/archive/2013/01/06/dumpanalysis.html 转来当笔记^_^ jstack Dump ...

  6. [置顶] Spring的自动装配

    采用构造函数注入,以及setter方法注入都需要写大量的XML配置文件,这时可以采用另一种方式,就是自动装,由Spring来给我们自动装配我们的Bean. Spring提供了四种自动装配类型 1:By ...

  7. HDU 4714 Tree2cycle (树形DP)

    Tree2cycle Time Limit: 15000/8000 MS (Java/Others)    Memory Limit: 102400/102400 K (Java/Others)Tot ...

  8. 2013-8-6 10:56:07 JAVA_WEB:员工号自动生成源代码

    create table user_info_temp (       usId varchar2(20),       usNo varchar2(20),       usName varchar ...

  9. AES加密时抛出java.security.InvalidKeyException: Illegal key size or default parametersIllegal key size or default parameters

    使用AES加密时,当密钥大于128时,代码会抛出java.security.InvalidKeyException: Illegal key size or default parameters Il ...

  10. 重载 UINavigationController 设置左侧返回按钮的文字为图片

    UINavigationController 导航栏控制器的左侧返回按钮如果需要设置成图片,仅使用系统的是无法实现的,需要重载系统的导航栏控制器,在控制器推出之前替换掉leftBarButtonIte ...