android|【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()马上触发下一次重绘,也就是:
class MyView extends View { public void onDraw(Canvas canvas) { super.onDraw(canvas); invalidate(); } }

这样一来,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:
public class AnimView extends View { public AnimView(Context context, AttributeSet attrs) { super(context, attrs); }public AnimView(Context context) { super(context); } }

自定义的“动画驱动”

有了载体,接下来需要的是我们的关键先生“动画驱动”,为了降低耦合和模块独立,这个驱动类不能做任何跟绘制相关的东西,仅仅做驱动的事情:
/** * 控制动画帧,单独一个模块 * @author zhanghuijun */ public class AnimFrameController {public static final String TAG = "AnimDemo AnimFrameController"; /** * 是否已经开始绘制 */ private boolean mIsStart = false; /** * 绘制Handler */ private Handler mDrawHandler = null; /** * 上次绘制时间 */ private long mLastDrawBeginTime = 0l; /** * 帧频,默认三十帧 */ private int mFtp = 30; /** * 刷新帧时间,默认三十帧 */ private long mIntervalTime = 1000 / 30; /** * 统计帧频所用 */ private int mFrameCount = 0; private long mStartTime = 0l; /** * IAnimFrameCallback */ private IAnimFrameListener mListener = null; /** * 构造器 */ public AnimFrameController(IAnimFrameListener listener, Looper threadLooper) { if (listener == null) { throw new RuntimeException("AnimFrameController 构造参数listener 不能为null"); } mListener = listener; mDrawHandler = new Handler(threadLooper); }/** * 开始渲染绘制动画 */ public void start() { if (!mIsStart) { mIsStart = true; mDrawHandler.post(mUpdateFrame); } }/** * 停止渲染绘制动画 */ public void stop() { if (mIsStart) { mIsStart = false; } }/** * 设置帧频,理想值,一般没那么精准 */ public void setFtp(int ftp) { if (ftp > 0) { mFtp = ftp; mIntervalTime = 1000 / mFtp; } }/** * 在每帧更新完毕时调用 */ public void updateFrame() { // 计算需要延迟的时间 long passTime = System.currentTimeMillis() - mLastDrawBeginTime; final long delayTime = mIntervalTime - passTime; // 延迟一定时间去绘制下一帧 if (delayTime > 0) { mDrawHandler.postDelayed(mUpdateFrame, delayTime); } else { mDrawHandler.post(mUpdateFrame); } // 统计帧频,如是未开始计时, 或帧时间太长(可能是由于动画暂时停止了,需要忽略这次计数据)则重置开始 if (mStartTime == 0 || System.currentTimeMillis() - mStartTime >= 1100) { mStartTime = System.currentTimeMillis(); mFrameCount = 0; } else { mFrameCount++; if (System.currentTimeMillis() - mStartTime >= 1000) { Log.d(TAG, "帧频为 : " + mFrameCount + " 帧一秒 "); mStartTime = System.currentTimeMillis(); ; mFrameCount = 0; } } }/** * 刷新帧Runnable */ private final Runnable mUpdateFrame = new Runnable() {@Override public void run() { if (!mIsStart) { return; } // 记录时间,每帧开始更新的时间 mLastDrawBeginTime = System.currentTimeMillis(); // 通知界面绘制帧 mListener.onUpdateFrame(); } }; /** * 动画View要实现的接口 */ public interface IAnimFrameListener { /** * 需要刷新帧 */ public void onUpdateFrame(); /** * 设置帧频 */ public void setFtp(int ftp); } }

上面的“驱动”主要控制了帧频和触发绘制,整个流程由这个驱动把关,结合View的实现看看框架的作用:
/** * 用于动画绘图的View * * @author zhanghuijun * */ public class AnimView extends View implements IAnimFrameListener, IAnimView {/** * 是否已经测量完成 */ protected boolean mHadSize = false; /** * 动画帧控制器 */ protected AnimFrameController mAnimFrameController = null; public AnimView(Context context, AttributeSet attrs) { super(context, attrs); init(); }public AnimView(Context context) { super(context); init(); }/** * 初始化 */ protected void init() { // 获取主线程的Looper,即发送给该Handler的都在主线程执行 mAnimFrameController = new AnimFrameController(this, Looper.getMainLooper()); }@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mHadSize = true; mWidth = w; // 其实就等于getMeasuredWidth()和getMeasuredHeight() mHeight = h; start(); }@Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); if (visibility == View.VISIBLE) { if (mHadSize) { start(); } } else { stop(); } }@Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); stop(); }/** * 开始 */ @Override public void start() { mAnimFrameController.start(); }/** * 停止 */ @Override public void stop() { mAnimFrameController.stop(); }/** * 设置帧频 */ @Override public void setFtp(int ftp) { mAnimFrameController.setFtp(ftp); }/** * 绘制 */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mAnimFrameController.updateFrame(); }@Override public void onUpdateFrame() { invalidate(); } }

首先,初始化的是创建一个驱动:
mAnimFrameController = new AnimFrameController(this, Looper.getMainLooper());

此处传了一个主线程的Looper过去,主要给AnimFrameController那个提供一个Looper,如果熟悉Handler的话,就会明白此处发送给该Looper的消息最终会在主线程执行。
然后,在View的onDraw()的结尾调用mAnimFrameController.updateFrame(); ,这样一来,所有要控制动画的东西都交给了AnimFrameController处理;
/** * 在每帧更新完毕时调用 */ public void updateFrame() { // 计算需要延迟的时间 long passTime = System.currentTimeMillis() - mLastDrawBeginTime; final long delayTime = mIntervalTime - passTime; // 延迟一定时间去绘制下一帧 if (delayTime > 0) { mDrawHandler.postDelayed(mUpdateFrame, delayTime); } else { mDrawHandler.post(mUpdateFrame); } ... }

在updateFrame()中,按照一定时间去延时绘制下一帧,从而达到控制动画绘制的帧频。
mUpdateFrame是一个Runnable:
/** * 刷新帧Runnable */ private final Runnable mUpdateFrame = new Runnable() {@Override public void run() { if (!mIsStart) { return; } // 记录时间,每帧开始更新的时间 mLastDrawBeginTime = System.currentTimeMillis(); // 通知界面绘制帧 mListener.onUpdateFrame(); } };

该Runnable的工作就是记录上一次绘制的时间,用来计算延迟时间;同时通知View去重新绘制,此处用了监听者模式,调用mListener.onUpdateFrame(); 就会回调到View去执行,从而将所有绘制操作交给View,AnimFrameController对于一概不管。
这样一来,“驱动”就完成了,这个“驱动”完全可以搬出去给其他有实现的View用。
动画时间

动画时间与常规的时间不会完全一致符合,原因有很多,而且它也不应该完全符合。试想一下,如果动画由于某些原因中断暂停了,那么动画中流逝的时间肯定也得中断;又或者有一个需求,需要让当前动画加快到两三倍速度,那么动画中的时间必须比正常时间快两三倍才正确。因此,我们需要一个“动画时钟类”来单独管理这个动画时间。
/** * 动画时钟,可自行扩张更多功能,如快进时间等 * @author zhanghuijun * */ public class AnimClock {/** * 相隔两帧之间的时间 */ private long mDeltaTime = 0l; /** * 上一帧的时间 */ private long mLastFrameTime = 0l; /** * 动画所经历的时间 */ private long mAnimTime = 0l; /** * 时钟启动,开始或者重新开始 */ public void start() { mLastFrameTime = System.currentTimeMillis(); }/** * 刷新帧时调用 */ public void updateFrame() { long now = System.currentTimeMillis(); mDeltaTime = now - mLastFrameTime; mAnimTime += mDeltaTime; mLastFrameTime = now; }/** * 获取相隔两帧之间的时间 * @return */ public long getDeltaTime() { return mDeltaTime; }/** * 获取动画总时间 * @return */ public long getAnimTime() { return mAnimTime; }}

具体结合请看源码,在最下面。
绘制的动画物体类AnimObject

要绘制一个纯动画,肯定会有很多个动画元素,这个“动画物体类AnimObject”就代表一个要绘制的动画元素。例如手机管家那个动图中,火箭,底下的发射台,飞起来之后的雾都应该是一个单独的绘制元素,然后整个动画就是绘制这些元素的变化。
/** * 动画绘制基础类 * @author zhanghuijun * */ public class AnimObject {/** * 是否需要绘制 */ private boolean mIsNeedDraw = true; /** * 父AnimObject */ private AnimObjectGroup mParent = null; /** * 根AnimView */ private View mRootAnimView = null; /** * 整个动画场景的宽高 */ private int mSceneWidth = 0; private int mSceneHeight = 0; /** * Context */ private Context mContext = null; public AnimObject(View mRootAnimView, Context mContext) { this.mRootAnimView = mRootAnimView; this.mContext = mContext; mSceneWidth = ((IAnimView) mRootAnimView).getAnimSceneWidth(); mSceneHeight = ((IAnimView) mRootAnimView).getAnimSceneHeight(); }/** * 绘制 */ public void draw(Canvas canvas, int sceneWidth, int sceneHeight) { }/** * 逻辑 */ public void logic(long animTime, long deltaTime) { }/** * 动画场景大小改变 */ public void onSizeChange(int w, int h) { mSceneWidth = w; mSceneHeight = h; } }

主要的功能是logic和draw,有进行业务逻辑的时候,则调用logic接口;而要绘制出来的时候,则调用其draw接口。
因为有些动画元素在划分可能会有组的概念,所以会有一个AnimObjectGroup类负责管理自己组内的AnimObject,这样写的好处与ViewGroup、View的写法无异。
最后,AnimView则作为动画元素的根元素,统一筹划所有子动画元素,因此完整的AnimView就是这样:
/** * 用于动画绘图的View * * @author zhanghuijun * */ public class AnimView extends View implements IAnimFrameListener, IAnimView {/** * 是否已经测量完成 */ protected boolean mHadSize = false; /** * 宽高 */ protected int mWidth = 0; protected int mHeight = 0; /** * 一组AnimObjectGroup */ protected List mAnimObjectGroups = null; /** * 动画帧控制器 */ protected AnimFrameController mAnimFrameController = null; /** * 动画时钟 */ protected AnimClock mAnimClock = null; public AnimView(Context context, AttributeSet attrs) { super(context, attrs); init(); }public AnimView(Context context) { super(context); init(); }/** * 初始化 */ protected void init() { // 获取主线程的Looper,即发送给该Handler的都在主线程执行 mAnimFrameController = new AnimFrameController(this, Looper.getMainLooper()); mAnimObjectGroups = new ArrayList(); mAnimClock = new AnimClock(); }@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mHadSize = true; mWidth = w; // 其实就等于getMeasuredWidth()和getMeasuredHeight() mHeight = h; for (int i = 0; i < mAnimObjectGroups.size(); i++) { mAnimObjectGroups.get(i).onSizeChange(w, h); } start(); }@Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); if (visibility == View.VISIBLE) { if (mHadSize) { start(); } } else { stop(); } }@Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); stop(); }/** * 开始 */ @Override public void start() { mAnimFrameController.start(); mAnimClock.start(); }/** * 停止 */ @Override public void stop() { mAnimFrameController.stop(); }/** * 添加一个AnimObjectGroup */ @Override public void addAnimObjectGroup(AnimObjectGroup group) { mAnimObjectGroups.add(group); }/** * 移除一个AnimObjectGroup */ @Override public void removeAnimObjectGroup(AnimObjectGroup group) { mAnimObjectGroups.remove(group); }@Override public int getAnimSceneWidth() { return mWidth; }@Override public int getAnimSceneHeight() { return mHeight; }/** * 设置帧频 */ @Override public void setFtp(int ftp) { mAnimFrameController.setFtp(ftp); }/** * 绘制 */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 逻辑 for (int i = 0; i < mAnimObjectGroups.size(); i++) { mAnimObjectGroups.get(i).logic(mAnimClock.getAnimTime(), mAnimClock.getDeltaTime()); } // 绘制 for (int i = 0; i < mAnimObjectGroups.size(); i++) { mAnimObjectGroups.get(i).draw(canvas, mWidth, mHeight); } mAnimFrameController.updateFrame(); mAnimClock.updateFrame(); }@Override public void onUpdateFrame() { invalidate(); }}

简单实例

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

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

声明

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


源码下载
【android|【Android进阶】如何写一个很屌的动画(1)---先实现一个简易的自定义动画框架】http://download.csdn.net/detail/scnuxisan225/9387333

    推荐阅读