当前位置:   article > 正文

【Android进阶】如何写一个很屌的动画(1)---先实现一个简易的自定义动画框架

【Android进阶】如何写一个很屌的动画(1)---先实现一个简易的自定义动画框架

系列中其他文章:

【Android进阶】如何写一个很屌的动画(1)---先实现一个简易的自定义动画框架

【Android进阶】如何写一个很屌的动画(2)---动画的好帮手们

【Android进阶】如何写一个很屌的动画(3)---高仿腾讯手机管家火箭动画

文章中充满了很多很大的Gif图,请耐心等待加载或者刷新页面,谢谢~


前言 


动画有多么重要,相信大家都清楚。它可以让一个枯燥乏味的静态界面变成一个充满动力的动画世界,提高用户体验。它的用途有很多,例如:

  • 让原本突兀的过程变得缓和,例如UC浏览器点击“酷站”,如下图 

  • 当有一个逻辑复杂,需要时间的来做,可以用动画来表示体现,例如腾讯手机管家在屏幕中清理内存,如下图 

可见,动画是多么的重要。可是,在Android中,动画有很多种展示形式,有很多中方案实现,例如有View动画,属性动画,帧动画等,但你们会发现,仅仅用Animation或者Animator难以实现上面动图中的动画,那些动画又是如何实现呢?

这就是本系列文章的重点所在。其实只要理解动画的本质,就会很容易做出任何动画,无论是普通的平移缩放动画,还是复杂的酷炫动画。在系列后期的文章里会写一个实例来实现“高仿手机管家内存清理的动画”,就是上面动图的动画。

一些基础知识: 
如果对Android中的动画知识认知不多,可以先看看这文章:Android 动画基础


理解Android中动画实现的本质 


在理解Android中动画实现的本质之前,首先要理解动画实现的原理,估计这个大家都清楚。 
无论是电影,动画片,游戏还是我们Android中的动画,其原理都是利用人类眼睛的“视觉残留”的特性医学证明人类具有“视觉暂留”的特性,人的眼睛看到一幅画或一个物体后,在1/24秒内不会消失。利用这一原理,在一幅画还没有消失前播放下一幅画,就会给人造成一种流畅的视觉变化效果。

也就是说,只要一秒内有连续24帧的画面连贯出现,那么看起来就是动画了。这也是我们Android中展示动画的原理,那么具体是怎么实现呢?

如果要在Android中实现动画展示,那么就必须要有一个“动画驱动”每隔1/24秒去调用View的draw()方法,同时改变每一帧中View需要变化的元素,让这个View不断的绘制,这样一来,所有变化就是组合成一个流畅的动画。

上面就是“Android中动画实现的本质”,其关键就是要有一个“动画驱动”。回想下我们平时最常用的动画类Animation或者Animator,其实它们内部实现也是一个“动画驱动”,驱动View不断绘制。所以,我们完全可以不用Animation或者Animator去做动画,只要有一个“驱动”即可,例如Scroller是个不错的选择,甚至我们可以写一个我们自己实现的“动画驱动”


常用的“动画驱动”

1、 View本身

最简单的“动画驱动”就是View本身,其最简单的实现就是在onDraw()马上触发下一次重绘,也就是:

  1. class MyView extends View {
  2. public void onDraw(Canvas canvas) {
  3. super.onDraw(canvas);
  4. invalidate();
  5. }
  6. }

这样一来,View每次绘制都是触发下一次绘制,不过你不用担心它一秒会绘制上百帧,Andriod应该是做了优化,正常情况下,这样的实现方案一秒最多60帧,而60帧已经是非常流畅的一个帧数了(一般情况下24帧已经足够)。这种方案的“驱动”比较适合在有一定实现的View上用,并且动画的东西与View的实现有关,例如TextView做一个文字变动的动画等。

延伸阅读:为什么认为游戏帧数要到 60 帧每秒才流畅,而大部分电影帧数只有 24 帧每秒? 


2、View动画,属性动画(Animation/Animator) 
关于这点的知识网上有太多太多,而且总结得非常好,或者还是可以看看这篇文章:Android 动画基础


3、Scroller 
有接触过界面滑动,应该对Scroller也有一定的认知,它需要结合View的computeScroll()方法实现。

这个“驱动”如它名字所示的,比较适合滑动相关的操作,因为它启动动画的参数就是位置的值。当然,你要用它来做点别的什么动画,也是完全没问题的。


4、自己实现一个简易的“动画驱动” 
既然有些需求用原有的方法难以实现或者实现起来不太合适,这个时候我们就需要自己动手了。因此,我也写了一个简易的“动画驱动”,同时扩展了一些额外的动画属性,可以方便的实现各种需求,具体请看下文。

这种驱动最大的优点就是所以东西都可以自己控制,例如控制帧频,控制动画的时间流逝速度等等,你想怎样就怎样。


自定义简易的动画框架 

这也是本文的重点,也是后面实现“高仿手机管家内存清理的动画”的基础。最下面有源码下载地址。

很长很长,现在不看也没事,可以先看下一篇,第三篇文章(【Android进阶】如何写一个很屌的动画(3)---高仿腾讯手机管家火箭动画)会详细说说这个动画框架如何设计和实现。

这个框架,在“动画驱动”上,使用的是自己写的“驱动”,其原理也是不断让界面重绘,同时可以控制一些驱动的参数,例如帧频等;在绘制上,则尽量仿造现在View框架来写,接下来我将详细说明。

首先说说这个框架的用途:主要用于绘制一些纯动画的界面,例如上面手机管家的动图那些界面。

既然是纯动画,那这个动画的载体直接用View或者SurfaceView即可。我比倾向直接用View,因为SurfaceView不支持硬件加速,而开启了硬件加速的View绘制效率比SurfaceView要好。 
所以,框架的载体就是一个继承View的AnimView:

  1. public class AnimView extends View {
  2. public AnimView(Context context, AttributeSet attrs) {
  3. super(context, attrs);
  4. }
  5. public AnimView(Context context) {
  6. super(context);
  7. }
  8. }

自定义的“动画驱动” 


有了载体,接下来需要的是我们的关键先生“动画驱动”,为了降低耦合和模块独立,这个驱动类不能做任何跟绘制相关的东西,仅仅做驱动的事情:

  1. /**
  2. * 控制动画帧,单独一个模块
  3. * @author zhanghuijun
  4. */
  5. public class AnimFrameController {
  6. public static final String TAG = "AnimDemo AnimFrameController";
  7. /**
  8. * 是否已经开始绘制
  9. */
  10. private boolean mIsStart = false;
  11. /**
  12. * 绘制Handler
  13. */
  14. private Handler mDrawHandler = null;
  15. /**
  16. * 上次绘制时间
  17. */
  18. private long mLastDrawBeginTime = 0l;
  19. /**
  20. * 帧频,默认三十帧
  21. */
  22. private int mFtp = 30;
  23. /**
  24. * 刷新帧时间,默认三十帧
  25. */
  26. private long mIntervalTime = 1000 / 30;
  27. /**
  28. * 统计帧频所用
  29. */
  30. private int mFrameCount = 0;
  31. private long mStartTime = 0l;
  32. /**
  33. * IAnimFrameCallback
  34. */
  35. private IAnimFrameListener mListener = null;
  36. /**
  37. * 构造器
  38. */
  39. public AnimFrameController(IAnimFrameListener listener, Looper threadLooper) {
  40. if (listener == null) {
  41. throw new RuntimeException("AnimFrameController 构造参数listener 不能为null");
  42. }
  43. mListener = listener;
  44. mDrawHandler = new Handler(threadLooper);
  45. }
  46. /**
  47. * 开始渲染绘制动画
  48. */
  49. public void start() {
  50. if (!mIsStart) {
  51. mIsStart = true;
  52. mDrawHandler.post(mUpdateFrame);
  53. }
  54. }
  55. /**
  56. * 停止渲染绘制动画
  57. */
  58. public void stop() {
  59. if (mIsStart) {
  60. mIsStart = false;
  61. }
  62. }
  63. /**
  64. * 设置帧频,理想值,一般没那么精准
  65. */
  66. public void setFtp(int ftp) {
  67. if (ftp > 0) {
  68. mFtp = ftp;
  69. mIntervalTime = 1000 / mFtp;
  70. }
  71. }
  72. /**
  73. * 在每帧更新完毕时调用
  74. */
  75. public void updateFrame() {
  76. // 计算需要延迟的时间
  77. long passTime = System.currentTimeMillis() - mLastDrawBeginTime;
  78. final long delayTime = mIntervalTime - passTime;
  79. // 延迟一定时间去绘制下一帧
  80. if (delayTime > 0) {
  81. mDrawHandler.postDelayed(mUpdateFrame, delayTime);
  82. } else {
  83. mDrawHandler.post(mUpdateFrame);
  84. }
  85. // 统计帧频,如是未开始计时, 或帧时间太长(可能是由于动画暂时停止了,需要忽略这次计数据)则重置开始
  86. if (mStartTime == 0 || System.currentTimeMillis() - mStartTime >= 1100) {
  87. mStartTime = System.currentTimeMillis();
  88. mFrameCount = 0;
  89. } else {
  90. mFrameCount++;
  91. if (System.currentTimeMillis() - mStartTime >= 1000) {
  92. Log.d(TAG, "帧频为 : " + mFrameCount + " 帧一秒 ");
  93. mStartTime = System.currentTimeMillis();;
  94. mFrameCount = 0;
  95. }
  96. }
  97. }
  98. /**
  99. * 刷新帧Runnable
  100. */
  101. private final Runnable mUpdateFrame = new Runnable() {
  102. @Override
  103. public void run() {
  104. if (!mIsStart) {
  105. return;
  106. }
  107. // 记录时间,每帧开始更新的时间
  108. mLastDrawBeginTime = System.currentTimeMillis();
  109. // 通知界面绘制帧
  110. mListener.onUpdateFrame();
  111. }
  112. };
  113. /**
  114. * 动画View要实现的接口
  115. */
  116. public interface IAnimFrameListener {
  117. /**
  118. * 需要刷新帧
  119. */
  120. public void onUpdateFrame();
  121. /**
  122. * 设置帧频
  123. */
  124. public void setFtp(int ftp);
  125. }
  126. }

上面的“驱动”主要控制了帧频和触发绘制,整个流程由这个驱动把关,结合View的实现看看框架的作用:

  1. /**
  2. * 用于动画绘图的View
  3. *
  4. * @author zhanghuijun
  5. *
  6. */
  7. public class AnimView extends View implements IAnimFrameListener, IAnimView {
  8. /**
  9. * 是否已经测量完成
  10. */
  11. protected boolean mHadSize = false;
  12. /**
  13. * 动画帧控制器
  14. */
  15. protected AnimFrameController mAnimFrameController = null;
  16. public AnimView(Context context, AttributeSet attrs) {
  17. super(context, attrs);
  18. init();
  19. }
  20. public AnimView(Context context) {
  21. super(context);
  22. init();
  23. }
  24. /**
  25. * 初始化
  26. */
  27. protected void init() {
  28. // 获取主线程的Looper,即发送给该Handler的都在主线程执行
  29. mAnimFrameController = new AnimFrameController(this, Looper.getMainLooper());
  30. }
  31. @Override
  32. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  33. super.onSizeChanged(w, h, oldw, oldh);
  34. mHadSize = true;
  35. mWidth = w; // 其实就等于getMeasuredWidth()和getMeasuredHeight()
  36. mHeight = h;
  37. start();
  38. }
  39. @Override
  40. protected void onWindowVisibilityChanged(int visibility) {
  41. super.onWindowVisibilityChanged(visibility);
  42. if (visibility == View.VISIBLE) {
  43. if (mHadSize) {
  44. start();
  45. }
  46. } else {
  47. stop();
  48. }
  49. }
  50. @Override
  51. protected void onDetachedFromWindow() {
  52. super.onDetachedFromWindow();
  53. stop();
  54. }
  55. /**
  56. * 开始
  57. */
  58. @Override
  59. public void start() {
  60. mAnimFrameController.start();
  61. }
  62. /**
  63. * 停止
  64. */
  65. @Override
  66. public void stop() {
  67. mAnimFrameController.stop();
  68. }
  69. /**
  70. * 设置帧频
  71. */
  72. @Override
  73. public void setFtp(int ftp) {
  74. mAnimFrameController.setFtp(ftp);
  75. }
  76. /**
  77. * 绘制
  78. */
  79. @Override
  80. protected void onDraw(Canvas canvas) {
  81. super.onDraw(canvas);
  82. mAnimFrameController.updateFrame();
  83. }
  84. @Override
  85. public void onUpdateFrame() {
  86. invalidate();
  87. }
  88. }

首先,初始化的是创建一个驱动:

mAnimFrameController = new AnimFrameController(this, Looper.getMainLooper());

此处传了一个主线程的Looper过去,主要给AnimFrameController那个提供一个Looper,如果熟悉Handler的话,就会明白此处发送给该Looper的消息最终会在主线程执行。

然后,在View的onDraw()的结尾调用mAnimFrameController.updateFrame();,这样一来,所有要控制动画的东西都交给了AnimFrameController处理;

  1. /**
  2. * 在每帧更新完毕时调用
  3. */
  4. public void updateFrame() {
  5. // 计算需要延迟的时间
  6. long passTime = System.currentTimeMillis() - mLastDrawBeginTime;
  7. final long delayTime = mIntervalTime - passTime;
  8. // 延迟一定时间去绘制下一帧
  9. if (delayTime > 0) {
  10. mDrawHandler.postDelayed(mUpdateFrame, delayTime);
  11. } else {
  12. mDrawHandler.post(mUpdateFrame);
  13. }
  14. ...
  15. }

在updateFrame()中,按照一定时间去延时绘制下一帧,从而达到控制动画绘制的帧频。 
mUpdateFrame是一个Runnable:

  1. /**
  2. * 刷新帧Runnable
  3. */
  4. private final Runnable mUpdateFrame = new Runnable() {
  5. @Override
  6. public void run() {
  7. if (!mIsStart) {
  8. return;
  9. }
  10. // 记录时间,每帧开始更新的时间
  11. mLastDrawBeginTime = System.currentTimeMillis();
  12. // 通知界面绘制帧
  13. mListener.onUpdateFrame();
  14. }
  15. };

该Runnable的工作就是记录上一次绘制的时间,用来计算延迟时间;同时通知View去重新绘制,此处用了监听者模式,调用mListener.onUpdateFrame();就会回调到View去执行,从而将所有绘制操作交给View,AnimFrameController对于一概不管。

这样一来,“驱动”就完成了,这个“驱动”完全可以搬出去给其他有实现的View用。

动画时间 


动画时间与常规的时间不会完全一致符合,原因有很多,而且它也不应该完全符合。试想一下,如果动画由于某些原因中断暂停了,那么动画中流逝的时间肯定也得中断;又或者有一个需求,需要让当前动画加快到两三倍速度,那么动画中的时间必须比正常时间快两三倍才正确。因此,我们需要一个“动画时钟类”来单独管理这个动画时间。

  1. /**
  2. * 动画时钟,可自行扩张更多功能,如快进时间等
  3. * @author zhanghuijun
  4. *
  5. */
  6. public class AnimClock {
  7. /**
  8. * 相隔两帧之间的时间
  9. */
  10. private long mDeltaTime = 0l;
  11. /**
  12. * 上一帧的时间
  13. */
  14. private long mLastFrameTime = 0l;
  15. /**
  16. * 动画所经历的时间
  17. */
  18. private long mAnimTime = 0l;
  19. /**
  20. * 时钟启动,开始或者重新开始
  21. */
  22. public void start() {
  23. mLastFrameTime = System.currentTimeMillis();
  24. }
  25. /**
  26. * 刷新帧时调用
  27. */
  28. public void updateFrame() {
  29. long now = System.currentTimeMillis();
  30. mDeltaTime = now - mLastFrameTime;
  31. mAnimTime += mDeltaTime;
  32. mLastFrameTime = now;
  33. }
  34. /**
  35. * 获取相隔两帧之间的时间
  36. * @return
  37. */
  38. public long getDeltaTime() {
  39. return mDeltaTime;
  40. }
  41. /**
  42. * 获取动画总时间
  43. * @return
  44. */
  45. public long getAnimTime() {
  46. return mAnimTime;
  47. }
  48. }

具体结合请看源码,在最下面。

绘制的动画物体类AnimObject 


要绘制一个纯动画,肯定会有很多个动画元素,这个“动画物体类AnimObject”就代表一个要绘制的动画元素。例如手机管家那个动图中,火箭,底下的发射台,飞起来之后的雾都应该是一个单独的绘制元素,然后整个动画就是绘制这些元素的变化。

  1. /**
  2. * 动画绘制基础类
  3. * @author zhanghuijun
  4. *
  5. */
  6. public class AnimObject {
  7. /**
  8. * 是否需要绘制
  9. */
  10. private boolean mIsNeedDraw = true;
  11. /**
  12. * 父AnimObject
  13. */
  14. private AnimObjectGroup mParent = null;
  15. /**
  16. * 根AnimView
  17. */
  18. private View mRootAnimView = null;
  19. /**
  20. * 整个动画场景的宽高
  21. */
  22. private int mSceneWidth = 0;
  23. private int mSceneHeight = 0;
  24. /**
  25. * Context
  26. */
  27. private Context mContext = null;
  28. public AnimObject(View mRootAnimView, Context mContext) {
  29. this.mRootAnimView = mRootAnimView;
  30. this.mContext = mContext;
  31. mSceneWidth = ((IAnimView) mRootAnimView).getAnimSceneWidth();
  32. mSceneHeight = ((IAnimView) mRootAnimView).getAnimSceneHeight();
  33. }
  34. /**
  35. * 绘制
  36. */
  37. public void draw(Canvas canvas, int sceneWidth, int sceneHeight) {
  38. }
  39. /**
  40. * 逻辑
  41. */
  42. public void logic(long animTime, long deltaTime) {
  43. }
  44. /**
  45. * 动画场景大小改变
  46. */
  47. public void onSizeChange(int w, int h) {
  48. mSceneWidth = w;
  49. mSceneHeight = h;
  50. }
  51. }

主要的功能是logic和draw,有进行业务逻辑的时候,则调用logic接口;而要绘制出来的时候,则调用其draw接口。

因为有些动画元素在划分可能会有组的概念,所以会有一个AnimObjectGroup类负责管理自己组内的AnimObject,这样写的好处与ViewGroup、View的写法无异。

最后,AnimView则作为动画元素的根元素,统一筹划所有子动画元素,因此完整的AnimView就是这样:

  1. /**
  2. * 用于动画绘图的View
  3. *
  4. * @author zhanghuijun
  5. *
  6. */
  7. public class AnimView extends View implements IAnimFrameListener, IAnimView {
  8. /**
  9. * 是否已经测量完成
  10. */
  11. protected boolean mHadSize = false;
  12. /**
  13. * 宽高
  14. */
  15. protected int mWidth = 0;
  16. protected int mHeight = 0;
  17. /**
  18. * 一组AnimObjectGroup
  19. */
  20. protected List<AnimObjectGroup> mAnimObjectGroups = null;
  21. /**
  22. * 动画帧控制器
  23. */
  24. protected AnimFrameController mAnimFrameController = null;
  25. /**
  26. * 动画时钟
  27. */
  28. protected AnimClock mAnimClock = null;
  29. public AnimView(Context context, AttributeSet attrs) {
  30. super(context, attrs);
  31. init();
  32. }
  33. public AnimView(Context context) {
  34. super(context);
  35. init();
  36. }
  37. /**
  38. * 初始化
  39. */
  40. protected void init() {
  41. // 获取主线程的Looper,即发送给该Handler的都在主线程执行
  42. mAnimFrameController = new AnimFrameController(this, Looper.getMainLooper());
  43. mAnimObjectGroups = new ArrayList<AnimObjectGroup>();
  44. mAnimClock = new AnimClock();
  45. }
  46. @Override
  47. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  48. super.onSizeChanged(w, h, oldw, oldh);
  49. mHadSize = true;
  50. mWidth = w; // 其实就等于getMeasuredWidth()和getMeasuredHeight()
  51. mHeight = h;
  52. for (int i = 0; i < mAnimObjectGroups.size(); i++) {
  53. mAnimObjectGroups.get(i).onSizeChange(w, h);
  54. }
  55. start();
  56. }
  57. @Override
  58. protected void onWindowVisibilityChanged(int visibility) {
  59. super.onWindowVisibilityChanged(visibility);
  60. if (visibility == View.VISIBLE) {
  61. if (mHadSize) {
  62. start();
  63. }
  64. } else {
  65. stop();
  66. }
  67. }
  68. @Override
  69. protected void onDetachedFromWindow() {
  70. super.onDetachedFromWindow();
  71. stop();
  72. }
  73. /**
  74. * 开始
  75. */
  76. @Override
  77. public void start() {
  78. mAnimFrameController.start();
  79. mAnimClock.start();
  80. }
  81. /**
  82. * 停止
  83. */
  84. @Override
  85. public void stop() {
  86. mAnimFrameController.stop();
  87. }
  88. /**
  89. * 添加一个AnimObjectGroup
  90. */
  91. @Override
  92. public void addAnimObjectGroup(AnimObjectGroup group) {
  93. mAnimObjectGroups.add(group);
  94. }
  95. /**
  96. * 移除一个AnimObjectGroup
  97. */
  98. @Override
  99. public void removeAnimObjectGroup(AnimObjectGroup group) {
  100. mAnimObjectGroups.remove(group);
  101. }
  102. @Override
  103. public int getAnimSceneWidth() {
  104. return mWidth;
  105. }
  106. @Override
  107. public int getAnimSceneHeight() {
  108. return mHeight;
  109. }
  110. /**
  111. * 设置帧频
  112. */
  113. @Override
  114. public void setFtp(int ftp) {
  115. mAnimFrameController.setFtp(ftp);
  116. }
  117. /**
  118. * 绘制
  119. */
  120. @Override
  121. protected void onDraw(Canvas canvas) {
  122. super.onDraw(canvas);
  123. // 逻辑
  124. for (int i = 0; i < mAnimObjectGroups.size(); i++) {
  125. mAnimObjectGroups.get(i).logic(mAnimClock.getAnimTime(), mAnimClock.getDeltaTime());
  126. }
  127. // 绘制
  128. for (int i = 0; i < mAnimObjectGroups.size(); i++) {
  129. mAnimObjectGroups.get(i).draw(canvas, mWidth, mHeight);
  130. }
  131. mAnimFrameController.updateFrame();
  132. mAnimClock.updateFrame();
  133. }
  134. @Override
  135. public void onUpdateFrame() {
  136. invalidate();
  137. }
  138. }

简单实例 


尝试用上面的框架做一个计数器,非常简单,具体源码在下面的源码链接中,请看效果: 


这里写图片描述

声明 


该框架好多东西我还没有测试过,所以应该还存在挺多问题;同时它的功能实在薄弱,难以用在真正的项目上。写该框架的目的在于让更多的人明白如何写一个好动画,授人以渔。


源码下载

http://download.csdn.net/detail/scnuxisan225/9387333

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/332651
推荐阅读
相关标签
  

闽ICP备14008679号