赞
踩
系列中其他文章:
【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<AnimObjectGroup> 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<AnimObjectGroup>();
- 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();
- }
-
- }
简单实例
尝试用上面的框架做一个计数器,非常简单,具体源码在下面的源码链接中,请看效果:
声明
该框架好多东西我还没有测试过,所以应该还存在挺多问题;同时它的功能实在薄弱,难以用在真正的项目上。写该框架的目的在于让更多的人明白如何写一个好动画,授人以渔。
源码下载
http://download.csdn.net/detail/scnuxisan225/9387333
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。