概述

最近发现一个很好玩的动画库,纯代码实现的而不是通过图片叠加唬人的,觉得很有意思看了下源码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)中

  1. //draw river
  2. int riverSaveCount = canvas.save();//记录river当前的图层
  3. mPaint.setStyle(Paint.Style.STROKE);
  4. canvas.clipRect(fishRectF, Region.Op.DIFFERENCE);//关键,确保鱼会盖住水池矩形
  5. canvas.drawPath(createRiverPath(arcBounds), mPaint);
  6. canvas.restoreToCount(riverSaveCount);//直接弹出到指定id层,并且将其上的Layer全部弹出,让该层称为顶栈

在处理水塘时使用canvas的sava和restoreToCount的方法记录图层,其中restoreToCount根据传入记录图层id将其上面的Layer全部弹出,然后处理了细节确保后面鱼游在池塘上面canvas.clipRect(fishRectF, Region.Op.DIFFERENCE),接着就是画池塘的矩形,使用了drawPath,因此需要传入池塘的path

  1. /**
  2. * 画水池的Path
  3. *
  4. * @param arcBounds
  5. * @return
  6. */
  7. private Path createRiverPath(RectF arcBounds) {
  8. if (mRiverPath != null) {
  9. return mRiverPath;
  10. }
  11. mRiverPath = new Path();
  12. RectF rectF = new RectF(arcBounds.centerX() - mRiverWidth / 2.0f, arcBounds.centerY() - mRiverHeight / 2.0f,
  13. arcBounds.centerX() + mRiverWidth / 2.0f, arcBounds.centerY() + mRiverHeight / 2.0f);//中心点+宽高定出绘制池塘矩形的两个点
  14. rectF.inset(mStrokeWidth / 2.0f, mStrokeWidth / 2.0f);//画笔宽度过宽微调,正直变窄
  15. mRiverPath.addRect(rectF, Path.Direction.CW);//顺时针方向画一个矩形
  16. return mRiverPath;
  17. }

这个是用虚线画的矩形,因此在画笔mPaint中做了文章,在setupPaint中使用

mPaint.setPathEffect(new DashPathEffect(new float[]{mPathFullLineSize, mPathDottedLineSize}, mPathDottedLineSize));

来使画笔为虚线,由于画笔比较粗,所以根据画笔宽度inset微调了池塘矩形(右边是不微调),这时矩形画好池塘如右边所示

2、画鱼

【鱼头定点的位置】

  1. `private final float[] mFishHeadPos = new float[2];//初始化鱼头的位置`

作者这里并没有设置值,因为这个鱼头位置是通过pathmeasure设置进去的` mRiverMeasure.getPosTan(mRiverMeasure.getLength() * fishProgress, mFishHeadPos, null);//mRiverMeasure.getLength() * fishProgress的点放到mFishHeadPos中去

因此这里为了更好地拆解这个鱼的部分,这里给出了初始化的位置

  1. `private final float[] mFishHeadPos = {100, 100};//初始化鱼头的位置`

在draw(Canvas canvas, Rect bounds)中

  1. `//draw fish
  2. int fishSaveCount = canvas.save();//记录当前图层
  3. mPaint.setStyle(Paint.Style.FILL);//实心画笔
  4. canvas.rotate(mFishRotateDegrees, mFishHeadPos[0], mFishHeadPos[1]);//鱼身翻转的度数
  5. canvas.clipPath(createFishEyePath(mFishHeadPos[0], mFishHeadPos[1] - mFishHeight * 0.06f), Region.Op.DIFFERENCE);//鱼眼
  6. canvas.drawPath(createFishPath(mFishHeadPos[0], mFishHeadPos[1]), mPaint);
  7. canvas.restoreToCount(fishSaveCount);`

首先这里换成了实心画笔,由于鱼需要不断地翻转角度,这里通过rotate方法实现,然后就是

【画鱼眼】

  1. ` /**
  2. * 画鱼眼
  3. *
  4. * @param fishEyeCenterX
  5. * @param fishEyeCenterY
  6. * @return
  7. */
  8. private Path createFishEyePath(float fishEyeCenterX, float fishEyeCenterY) {
  9. Path path = new Path();
  10. path.addCircle(fishEyeCenterX, fishEyeCenterY, mFishEyeSize, Path.Direction.CW);
  11. return path;
  12. }`

比较简单,画了一个圆的path,然后使用Region.Op.DIFFERENCE来clip出来,接着要画鱼的身体了createFishPath(mFishHeadPos[0], mFishHeadPos[1])传入鱼头位置开始按照鱼头位置画(鱼头位置变化鱼身位置随之变化),下面来看看鱼身这个path如何画的

  1. ` /**
  2. * 根据鱼眼画鱼身体
  3. *
  4. * @param fishCenterX
  5. * @param fishCenterY
  6. * @return
  7. */
  8. private Path createFishPath(float fishCenterX, float fishCenterY) {
  9. Path path = new Path();
  10. float fishHeadX = fishCenterX;
  11. float fishHeadY = fishCenterY - mFishHeight / 2.0f;
  12. //the head of the fish
  13. path.moveTo(fishHeadX, fishHeadY);
  14. //the left body of the fish
  15. path.quadTo(fishHeadX - mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.222f, fishHeadX - mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.444f);
  16. path.lineTo(fishHeadX - mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.666f);
  17. path.lineTo(fishHeadX - mFishWidth * 0.5f, fishHeadY + mFishHeight * 0.8f);
  18. path.lineTo(fishHeadX - mFishWidth * 0.5f, fishHeadY + mFishHeight);
  19. //the tail of the fish
  20. path.lineTo(fishHeadX, fishHeadY + mFishHeight * 0.9f);
  21. //the right body of the fish
  22. path.lineTo(fishHeadX + mFishWidth * 0.5f, fishHeadY + mFishHeight);
  23. path.lineTo(fishHeadX + mFishWidth * 0.5f, fishHeadY + mFishHeight * 0.8f);
  24. path.lineTo(fishHeadX + mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.666f);
  25. path.lineTo(fishHeadX + mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.444f);
  26. path.quadTo(fishHeadX + mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.222f, fishHeadX, fishHeadY);
  27. path.close();
  28. return path;
  29. }

`

这里定位好鱼头先通过二阶贝塞尔曲线画出鱼身的弧线,然后通过直线lineTo画鱼尾巴,画完一边再画另一边,成型图如下所示

2、动起来

首先在抽象类LoadingRenderer中封装了基本的操作,其中一个就是使用了属性动画

  1. ` private void setupAnimators() {
  2. mRenderAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
  3. mRenderAnimator.setRepeatCount(Animation.INFINITE);
  4. mRenderAnimator.setRepeatMode(Animation.RESTART);//无线重复的方式
  5. mRenderAnimator.setInterpolator(new LinearInterpolator());
  6. mRenderAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  7. @Override
  8. public void onAnimationUpdate(ValueAnimator animation) {
  9. computeRender((float) animation.getAnimatedValue());
  10. invalidateSelf();
  11. }
  12. });
  13. }`

可以看出这里使用了0-1的渐变,然后将0-1渐变值传到抽象函数public abstract void computeRender(float renderProgress);中按照你的需求自己实现,这里Fish继承了这个类后是这样重写的

  1. ` @Override
  2. public void computeRender(float renderProgress) {
  3. if (mRiverPath == null) {
  4. return;
  5. }
  6. if (mRiverMeasure == null) {
  7. mRiverMeasure = new PathMeasure(mRiverPath, false);
  8. }
  9. float fishProgress = FISH_INTERPOLATOR.getInterpolation(renderProgress);
  10. mRiverMeasure.getPosTan(mRiverMeasure.getLength() * fishProgress, mFishHeadPos, null);
  11. mFishRotateDegrees = calculateRotateDegrees(fishProgress);
  12. }`

这个方法中信息量非常大,毕竟小鱼动起来全靠它了,我们来细细分析,首先按照river矩形得到其pathMeasure

mRiverMeasure = new PathMeasure(mRiverPath, false),

得到pathMeasure后通过

mRiverMeasure.getPosTan(mRiverMeasure.getLength() * fishProgress, mFishHeadPos, null);

将mRiverMeasure.getLength() * fishProgress处的坐标传到鱼头位置,这样鱼头位置在不停的变化,绘制鱼身的位置也随之变化。下面拉近镜头看看鱼头位置是怎样在变换.

秘密藏在

  1. `float fishProgress = FISH_INTERPOLATOR.getInterpolation(renderProgress);`

插值器是自定义的,插值器本质是时间的函数,定义了动画变化的规律,需要实现getInterpolation(float input)即可,自定义插值器如下

  1. ` private class FishInterpolator implements Interpolator {
  2. //自定义插值器
  3. @Override
  4. public float getInterpolation(float input) {
  5. int index = ((int) (input / FISH_MOVE_POINTS_RATE));
  6. if (index >= FISH_MOVE_POINTS.length) {
  7. index = FISH_MOVE_POINTS.length - 1;
  8. }
  9. return FISH_MOVE_POINTS[index];
  10. }
  11. }`

关于插值器和估值器可以查看http://blog.csdn.net/xsf50717/article/details/50472341

可见返回的是鱼初始游经的8个点在FISH_MOVE_POINTS数组中,这种鱼就会在这8个位置出现。出现后还要保持角度一致,这个任务就落在

mFishRotateDegrees = calculateRotateDegrees(fishProgress);

  1. ` private float calculateRotateDegrees(float fishProgress) {
  2. if (fishProgress < FISH_MOVE_POINTS_RATE * 2) {
  3. return 90;
  4. }
  5. if (fishProgress < FISH_MOVE_POINTS_RATE * 4) {
  6. return 180;
  7. }
  8. if (fishProgress < FISH_MOVE_POINTS_RATE * 6) {
  9. return 270;
  10. }
  11. return 0.0f;
  12. }`

变化的角度得到后,那么鱼儿动翻转就容易了,还记得在画鱼时候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()通过获取屏幕分辨率去适配

  1. `/调整适配
  2. final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
  3. final float screenDensity = metrics.density;
  4. mWidth = DEFAULT_WIDTH * screenDensity;
  5. mHeight = DEFAULT_HEIGHT * screenDensity;
  6. mStrokeWidth = DEFAULT_STROKE_WIDTH * screenDensity;`

这种方式也是在自定义控件中值得学习的

3、canvas、path、paint的API还是要熟练掌握

4、OOP+设计模式可以使得代码更加优雅,省去大量冗余代码,如本例的LoadingRender抽象类

拆轮子之Fish动画分析的更多相关文章

  1. iOS 手机淘宝加入购物车动画分析

    1.最终效果 仿淘宝动画 2.核心代码 _cartAnimView=[[UIImageView alloc] initWithFrame:CGRectMake(_propView.frame.size ...

  2. iOS手机淘宝加入购物车动画分析

    本文转载至 http://www.jianshu.com/p/e77e3ce8ee24 1.最终效果 仿淘宝动画 2.核心代码 _cartAnimView=[[UIImageView alloc] i ...

  3. ANDROID开机动画分析

    开机动画文件:bootanimation.zip在system\media文件夹下动画是由系列图片连续刷屏实现的..bootanimation.zip文件是zip压缩文件,压缩方式要求是存储压缩,包含 ...

  4. Android——Fragment过度动画分析一(转)

    Sliding Fragment 作者:小文字 出处:http://www.cnblogs.com/avenwu/   介绍:该案例为传统的Fragment增加了个性化的补间动画,其效果是原有frag ...

  5. Fragment过度动画分析一

    Sliding Fragment 介绍:该案例为传统的Fragment增加了个性化的补间动画,其效果是原有fragment向屏幕内做一定的下沉,新的fragment显示在最上层,产生层叠效果的多个fr ...

  6. jq动画分析1

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  7. jq动画分析

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  8. 史上最浅显易懂的RxJava入门教程

    RxJava是一个神奇的框架,用法很简单,但内部实现有点复杂,代码逻辑有点绕.我读源码时,确实有点似懂非懂的感觉.网上关于RxJava源码分析的文章,源码贴了一大堆,代码逻辑绕来绕去的,让人看得云里雾 ...

  9. Android OkHttp使用与分析

    安卓开发领域,很多重要的问题都有了很好的开源解决方案,例如网络请求 OkHttp + Retrofit 简直就是不二之选."我们不重复造轮子不表示我们不需要知道轮子该怎么造及如何更好的造!& ...

随机推荐

  1. J-Robot,能走、能跳舞的机器人

      最近一个月基本上没有更新博客了,主要是和朋友一起在捣鼓J-Robot这个机器人,现在基本是可以控制它了,也算是一点小小的成就感吧.   先来几张图片吧. 再来一张:   是否觉得呆呆的?来,Jim ...

  2. GDAL 2.0版本RPC校正速度测试

    GDAL2.0版本的更新日志中提到了对RPC校正的优化,今天测试了一下,发现提升的速度还是蛮快的,测试的数据是一个IRS-P5的数据. 单线程测试 首先使用一个线程进行测试,使用下面的批处理进行运行, ...

  3. Android存储之SharedPreferences

    Android数据存储之SharedPreferences SharedPreferences对象初始化 SharedPreferences mSharedPreferences = getShare ...

  4. 怎样在Ubuntu 14.04中搭建gitolite git服务器

     1.   首先这里我们安装openssh-serveropenssh-client,如果你用的是VPS之类的一般都默认安装好了,不过运行一个这个命令不会有错的,如果有安装就会提示已安装. sud ...

  5. python使用qq服务器发送邮件

    python使用qq服务器发送邮件 直接上代码: #!/usr/bin/python2.7 #-*- coding: UTF-8 -*- # sendmail.py # # init created: ...

  6. 使用Broadcast实现android组件之间的通信

    android组件之间的通信有多种实现方式,Broadcast就是其中一种.在activity和fragment之间的通信,broadcast用的更多本文以一个activity为例. 效果如图: 布局 ...

  7. 如何解决RK3168或者RK系列MASKROM的问题

    不知道使用RK芯片的小伙伴有没有遇到我这样的问题,在用Android-Tool下载相应 IMG的时候,正常情况下,按电源键和音量+键应该出现loader下载模式,但是却出现MASKROM的字样,以前不 ...

  8. 快速高分辨率图像的立体匹配方法Effective large scale stereo matching

    <Effective large scale stereo matching> In this paper we propose a novel approach to binocular ...

  9. 北大青鸟Asp.net之颗粒归仓

    自从小编走进编程的世界以来,学习的编程知识都是和C/S这个小伙伴握手,直到做完牛腩老师的新闻发布系统,才开始了小编的B/S学习生涯,和B/S初次谋面,小宇宙瞬间爆发了,看着自己的第一个B/S系统,牛腩 ...

  10. Android官方命令深入分析之etc1tool

    etc1tool是一个命令行工具,可以将PNG图像压缩为etc1标准,并且可以进行解压缩. 用法: etc1tool infile [--help | --encode | --encodeNoHea ...