拆轮子之Fish动画分析
概述
最近发现一个很好玩的动画库,纯代码实现的而不是通过图片叠加唬人的,觉得很有意思看了下源码https://github.com/dinuscxj/LoadingDrawable,
这个动画效果使用drawable来实现,觉得很好玩,先分析这个Fish动画(上面是鱼,下面是ghosteye,可是我看半天看不出哪里像 ghost ╮(╯▽╰)╭)。

类图
项目整体是采用了策略模式(Strategy)通过给LoadingDrawable设置不同的LoadingRenderer(渲染器) 来绘制不同的加载动画。Fish首先是继承了Drable类实现了Animate接口。
LoadingDrawable这个类继承Drawable并实现接口Animatable,构造函数必须传入 LoadingRenderer的子类。并通过回调Callback与LoadingRenderer进行交互。
LoadingRenderer主要负责给LoadingDrawable绘制的。 这里使用抽象类将公共使用的归类到该类处理,比如公共参数,宽高,描边,圆的默认半径等等。将绘制不同图形的功能函数如 draw(Canvas, Rect) 和 computeRender(float)抽象出来, 其中draw(Canvas, Rect)顾名思义,负责绘制, computeRender 负责计算当前的进度需要绘制的形状的大小,位置,其参数 是有类内部的成员变量mRenderAnimator负责传递。
这种将公共的封装抽象出来的OOP思想要注意掌握。

FishLoadingRender
在前面说了,关键是draw(Canvas,Rect)方法复制绘制图形, computeRender(float)负责让图片具体动起来,下面先对其核心分析一下。主要是三步走:
【画池塘(矩形框)】——>【画鱼】——>【动起来】
ok,一个个来分析,先拣软柿子捏,矩形框。
1、矩形框(池塘)
在draw(Canvas canvas, Rect bounds)中
//draw river
int riverSaveCount = canvas.save();//记录river当前的图层
mPaint.setStyle(Paint.Style.STROKE);
canvas.clipRect(fishRectF, Region.Op.DIFFERENCE);//关键,确保鱼会盖住水池矩形
canvas.drawPath(createRiverPath(arcBounds), mPaint);
canvas.restoreToCount(riverSaveCount);//直接弹出到指定id层,并且将其上的Layer全部弹出,让该层称为顶栈
在处理水塘时使用canvas的sava和restoreToCount的方法记录图层,其中restoreToCount根据传入记录图层id将其上面的Layer全部弹出,然后处理了细节确保后面鱼游在池塘上面canvas.clipRect(fishRectF, Region.Op.DIFFERENCE),接着就是画池塘的矩形,使用了drawPath,因此需要传入池塘的path
/**
* 画水池的Path
*
* @param arcBounds
* @return
*/
private Path createRiverPath(RectF arcBounds) {
if (mRiverPath != null) {
return mRiverPath;
}
mRiverPath = new Path();
RectF rectF = new RectF(arcBounds.centerX() - mRiverWidth / 2.0f, arcBounds.centerY() - mRiverHeight / 2.0f,
arcBounds.centerX() + mRiverWidth / 2.0f, arcBounds.centerY() + mRiverHeight / 2.0f);//中心点+宽高定出绘制池塘矩形的两个点
rectF.inset(mStrokeWidth / 2.0f, mStrokeWidth / 2.0f);//画笔宽度过宽微调,正直变窄
mRiverPath.addRect(rectF, Path.Direction.CW);//顺时针方向画一个矩形
return mRiverPath;
}
这个是用虚线画的矩形,因此在画笔mPaint中做了文章,在setupPaint中使用
mPaint.setPathEffect(new DashPathEffect(new float[]{mPathFullLineSize, mPathDottedLineSize}, mPathDottedLineSize));
来使画笔为虚线,由于画笔比较粗,所以根据画笔宽度inset微调了池塘矩形(右边是不微调),这时矩形画好池塘如右边所示


2、画鱼
【鱼头定点的位置】
`private final float[] mFishHeadPos = new float[2];//初始化鱼头的位置`
作者这里并没有设置值,因为这个鱼头位置是通过pathmeasure设置进去的` mRiverMeasure.getPosTan(mRiverMeasure.getLength() * fishProgress, mFishHeadPos, null);//mRiverMeasure.getLength() * fishProgress的点放到mFishHeadPos中去
因此这里为了更好地拆解这个鱼的部分,这里给出了初始化的位置
`private final float[] mFishHeadPos = {100, 100};//初始化鱼头的位置`
在draw(Canvas canvas, Rect bounds)中
`//draw fish
int fishSaveCount = canvas.save();//记录当前图层
mPaint.setStyle(Paint.Style.FILL);//实心画笔
canvas.rotate(mFishRotateDegrees, mFishHeadPos[0], mFishHeadPos[1]);//鱼身翻转的度数
canvas.clipPath(createFishEyePath(mFishHeadPos[0], mFishHeadPos[1] - mFishHeight * 0.06f), Region.Op.DIFFERENCE);//鱼眼
canvas.drawPath(createFishPath(mFishHeadPos[0], mFishHeadPos[1]), mPaint);
canvas.restoreToCount(fishSaveCount);`
首先这里换成了实心画笔,由于鱼需要不断地翻转角度,这里通过rotate方法实现,然后就是
【画鱼眼】
` /**
* 画鱼眼
*
* @param fishEyeCenterX
* @param fishEyeCenterY
* @return
*/
private Path createFishEyePath(float fishEyeCenterX, float fishEyeCenterY) {
Path path = new Path();
path.addCircle(fishEyeCenterX, fishEyeCenterY, mFishEyeSize, Path.Direction.CW);
return path;
}`
比较简单,画了一个圆的path,然后使用Region.Op.DIFFERENCE来clip出来,接着要画鱼的身体了createFishPath(mFishHeadPos[0], mFishHeadPos[1])传入鱼头位置开始按照鱼头位置画(鱼头位置变化鱼身位置随之变化),下面来看看鱼身这个path如何画的
` /**
* 根据鱼眼画鱼身体
*
* @param fishCenterX
* @param fishCenterY
* @return
*/
private Path createFishPath(float fishCenterX, float fishCenterY) {
Path path = new Path();
float fishHeadX = fishCenterX;
float fishHeadY = fishCenterY - mFishHeight / 2.0f;
//the head of the fish
path.moveTo(fishHeadX, fishHeadY);
//the left body of the fish
path.quadTo(fishHeadX - mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.222f, fishHeadX - mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.444f);
path.lineTo(fishHeadX - mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.666f);
path.lineTo(fishHeadX - mFishWidth * 0.5f, fishHeadY + mFishHeight * 0.8f);
path.lineTo(fishHeadX - mFishWidth * 0.5f, fishHeadY + mFishHeight);
//the tail of the fish
path.lineTo(fishHeadX, fishHeadY + mFishHeight * 0.9f);
//the right body of the fish
path.lineTo(fishHeadX + mFishWidth * 0.5f, fishHeadY + mFishHeight);
path.lineTo(fishHeadX + mFishWidth * 0.5f, fishHeadY + mFishHeight * 0.8f);
path.lineTo(fishHeadX + mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.666f);
path.lineTo(fishHeadX + mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.444f);
path.quadTo(fishHeadX + mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.222f, fishHeadX, fishHeadY);
path.close();
return path;
}
`
这里定位好鱼头先通过二阶贝塞尔曲线画出鱼身的弧线,然后通过直线lineTo画鱼尾巴,画完一边再画另一边,成型图如下所示

2、动起来
首先在抽象类LoadingRenderer中封装了基本的操作,其中一个就是使用了属性动画
` private void setupAnimators() {
mRenderAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
mRenderAnimator.setRepeatCount(Animation.INFINITE);
mRenderAnimator.setRepeatMode(Animation.RESTART);//无线重复的方式
mRenderAnimator.setInterpolator(new LinearInterpolator());
mRenderAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
computeRender((float) animation.getAnimatedValue());
invalidateSelf();
}
});
}`
可以看出这里使用了0-1的渐变,然后将0-1渐变值传到抽象函数public abstract void computeRender(float renderProgress);中按照你的需求自己实现,这里Fish继承了这个类后是这样重写的
` @Override
public void computeRender(float renderProgress) {
if (mRiverPath == null) {
return;
}
if (mRiverMeasure == null) {
mRiverMeasure = new PathMeasure(mRiverPath, false);
}
float fishProgress = FISH_INTERPOLATOR.getInterpolation(renderProgress);
mRiverMeasure.getPosTan(mRiverMeasure.getLength() * fishProgress, mFishHeadPos, null);
mFishRotateDegrees = calculateRotateDegrees(fishProgress);
}`
这个方法中信息量非常大,毕竟小鱼动起来全靠它了,我们来细细分析,首先按照river矩形得到其pathMeasure
mRiverMeasure = new PathMeasure(mRiverPath, false),
得到pathMeasure后通过
mRiverMeasure.getPosTan(mRiverMeasure.getLength() * fishProgress, mFishHeadPos, null);
将mRiverMeasure.getLength() * fishProgress处的坐标传到鱼头位置,这样鱼头位置在不停的变化,绘制鱼身的位置也随之变化。下面拉近镜头看看鱼头位置是怎样在变换.
秘密藏在
`float fishProgress = FISH_INTERPOLATOR.getInterpolation(renderProgress);`
插值器是自定义的,插值器本质是时间的函数,定义了动画变化的规律,需要实现getInterpolation(float input)即可,自定义插值器如下
` private class FishInterpolator implements Interpolator {
//自定义插值器
@Override
public float getInterpolation(float input) {
int index = ((int) (input / FISH_MOVE_POINTS_RATE));
if (index >= FISH_MOVE_POINTS.length) {
index = FISH_MOVE_POINTS.length - 1;
}
return FISH_MOVE_POINTS[index];
}
}`
关于插值器和估值器可以查看http://blog.csdn.net/xsf50717/article/details/50472341
可见返回的是鱼初始游经的8个点在FISH_MOVE_POINTS数组中,这种鱼就会在这8个位置出现。出现后还要保持角度一致,这个任务就落在
mFishRotateDegrees = calculateRotateDegrees(fishProgress);
` private float calculateRotateDegrees(float fishProgress) {
if (fishProgress < FISH_MOVE_POINTS_RATE * 2) {
return 90;
}
if (fishProgress < FISH_MOVE_POINTS_RATE * 4) {
return 180;
}
if (fishProgress < FISH_MOVE_POINTS_RATE * 6) {
return 270;
}
return 0.0f;
}`
变化的角度得到后,那么鱼儿动翻转就容易了,还记得在画鱼时候canvas.rotate(mFishRotateDegrees, mFishHeadPos[0], mFishHeadPos[1]);,这样就ok了,可以看到一开始时候鱼儿动起来的样子了
其他
1、本质还是个动画的drawable,主要是Drawable.Callback实现invalidateDrawable(Drawable d),scheduleDrawable(Drawable d, Runnable what, long when),unscheduleDrawable(Drawable d, Runnable what)实现回调联动。
2、作者这里为了防止不同手机分辨率的适配一开始定义了静态变量,然后在init()通过获取屏幕分辨率去适配
`/调整适配
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
final float screenDensity = metrics.density;
mWidth = DEFAULT_WIDTH * screenDensity;
mHeight = DEFAULT_HEIGHT * screenDensity;
mStrokeWidth = DEFAULT_STROKE_WIDTH * screenDensity;`
这种方式也是在自定义控件中值得学习的
3、canvas、path、paint的API还是要熟练掌握
4、OOP+设计模式可以使得代码更加优雅,省去大量冗余代码,如本例的LoadingRender抽象类
拆轮子之Fish动画分析的更多相关文章
- iOS 手机淘宝加入购物车动画分析
1.最终效果 仿淘宝动画 2.核心代码 _cartAnimView=[[UIImageView alloc] initWithFrame:CGRectMake(_propView.frame.size ...
- iOS手机淘宝加入购物车动画分析
本文转载至 http://www.jianshu.com/p/e77e3ce8ee24 1.最终效果 仿淘宝动画 2.核心代码 _cartAnimView=[[UIImageView alloc] i ...
- ANDROID开机动画分析
开机动画文件:bootanimation.zip在system\media文件夹下动画是由系列图片连续刷屏实现的..bootanimation.zip文件是zip压缩文件,压缩方式要求是存储压缩,包含 ...
- Android——Fragment过度动画分析一(转)
Sliding Fragment 作者:小文字 出处:http://www.cnblogs.com/avenwu/ 介绍:该案例为传统的Fragment增加了个性化的补间动画,其效果是原有frag ...
- Fragment过度动画分析一
Sliding Fragment 介绍:该案例为传统的Fragment增加了个性化的补间动画,其效果是原有fragment向屏幕内做一定的下沉,新的fragment显示在最上层,产生层叠效果的多个fr ...
- jq动画分析1
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- jq动画分析
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- 史上最浅显易懂的RxJava入门教程
RxJava是一个神奇的框架,用法很简单,但内部实现有点复杂,代码逻辑有点绕.我读源码时,确实有点似懂非懂的感觉.网上关于RxJava源码分析的文章,源码贴了一大堆,代码逻辑绕来绕去的,让人看得云里雾 ...
- Android OkHttp使用与分析
安卓开发领域,很多重要的问题都有了很好的开源解决方案,例如网络请求 OkHttp + Retrofit 简直就是不二之选."我们不重复造轮子不表示我们不需要知道轮子该怎么造及如何更好的造!& ...
随机推荐
- SQLite 分离数据库(http://www.w3cschool.cc/sqlite/sqlite-detach-database.html)
SQLite 分离数据库 SQLite的 DETACH DTABASE 语句是用来把命名数据库从一个数据库连接分离和游离出来,连接是之前使用 ATTACH 语句附加的.如果同一个数据库文件已经被附加上 ...
- Struts 1 之文件上传
Struts 1 对Apache的commons-fileupload进行了再封装,把上传文件封装成FormFile对象 定义UploadForm: private FormFilefile; //上 ...
- 一个 Linux 上分析死锁的简单方法
简介 死锁 (deallocks): 是指两个或两个以上的进程(线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这 ...
- 多线程之Java线程阻塞与唤醒
线程的阻塞和唤醒在多线程并发过程中是一个关键点,当线程数量达到很大的数量级时,并发可能带来很多隐蔽的问题.如何正确暂停一个线程,暂停后又如何在一个要求的时间点恢复,这些都需要仔细考虑的细节.在Java ...
- unix下各种包安装方法备忘
deb包 : sudo dpkg -i google-chrome-stable_amd64.deb
- UNIX网络编程——客户/服务器程序设计示范(四)
TCP预先派生子进程服务器程序,accept使用线程上锁保护 我们使用线程上锁保护accept,因为这种方法不仅适用于同一进程内各线程之间的上锁,而且适用于不同进程之间的上锁. ...
- C语言--测试电脑存储模式(大端存储OR小端存储)
相信大家都知道大端存储和小端存储的概念,这在平时,我们一般不用考虑,但是,在某些场合,这些概念就显得很重要,比如,在 Socket 通信时,我们的电脑是小端存储模式,可是传送数据或者消息给对方电脑时, ...
- (八十一)利用系统自带App来实现导航
利用系统的地图App进行导航,只需要传入起点和终点.启动参数,调用MKMapItem的类方法openMapWithItems:launchOptions:来实现定位,调用此方法后会打开系统的地图App ...
- 北大青鸟Asp.net之颗粒归仓
自从小编走进编程的世界以来,学习的编程知识都是和C/S这个小伙伴握手,直到做完牛腩老师的新闻发布系统,才开始了小编的B/S学习生涯,和B/S初次谋面,小宇宙瞬间爆发了,看着自己的第一个B/S系统,牛腩 ...
- C#弹出对话框
一.基于WINFORM下的选择对话框 在WINFORM下,我们可以利用系统的对话框(MessageBox)来实现,具体思路是读取MessageBox的返回值(YES或NO)来达到对操作的控制.下面是一 ...