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