链接:http://developer.android.com/reference/android/graphics/drawable/AnimationDrawable.html
http://developer.android.com/reference/android/graphics/drawable/DrawableContainer.html
在项目中实现一个欢迎页的逐帧动画(100张图,每张图片约3K大小,播放时间2.5秒),尝试了五种方法,都不能满足要求:
1. 使用 AnimationDrawable加载逐帧动画;
2. 使用 VideoView播放.3gp文件;
3. 使用 WebView加载.gif图片;
4. 使用 WebView播放 flash;
5. 自定义一个带图片回收机制的 View逐帧加载图片。
过程中遇到的问题及分析如下:
1. AnimationDrawable:图片过多,总的占用内存很大,导致 OOM异常。
在网上有很多简单的例子介绍 AnimationDrawable的用法,都是在 res/drawable下写一个帧序列,然后通过一个 ImageView对象把这个 Drawable作为背景或是图片资源加载出来。看 AnimationDrawable类的逐帧实现方法我们就可以发现,Android系统是把这些图片都预加载出来的。AnimationDrawable类中逐帧动画调用的方法依次是:start() - run() - nextFrame() - setFrame() - selectDrawable()...
public boolean selectDrawable(int idx) { if (idx == mCurIndex) { return false; } final long now = SystemClock.uptimeMillis(); if (DEBUG) android.util.Log.i(TAG, toString() + " from " + mCurIndex + " to " + idx + ": exit=" + mDrawableContainerState.mExitFadeDuration + " enter=" + mDrawableContainerState.mEnterFadeDuration); if (mDrawableContainerState.mExitFadeDuration > 0) { if (mLastDrawable != null) { mLastDrawable.setVisible(false, false); } if (mCurrDrawable != null) { mLastDrawable = mCurrDrawable; mExitAnimationEnd = now + mDrawableContainerState.mExitFadeDuration; } else { mLastDrawable = null; mExitAnimationEnd = 0; } } else if (mCurrDrawable != null) { mCurrDrawable.setVisible(false, false); } if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) { Drawable d = mDrawableContainerState.mDrawables[idx]; mCurrDrawable = d; mCurIndex = idx; if (d != null) { d.mutate(); if (mDrawableContainerState.mEnterFadeDuration > 0) { mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration; } else { d.setAlpha(mAlpha); } d.setVisible(isVisible(), true); d.setDither(mDrawableContainerState.mDither); d.setColorFilter(mColorFilter); d.setState(getState()); d.setLevel(getLevel()); d.setBounds(getBounds()); } } else { mCurrDrawable = null; mCurIndex = -1; } if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) { if (mAnimationRunnable == null) { mAnimationRunnable = new Runnable() { @Override public void run() { animate(true); invalidateSelf(); } }; } else { unscheduleSelf(mAnimationRunnable); } // Compute first frame and schedule next animation. animate(true); } invalidateSelf(); return true; }
在 selectDrawable()方法里,可以看到每一帧的加载,都是从 Drawable数组里找出当前序号对应的 Drawable对象(因为 AnimationDrawable类本身继承自 DrawableContainer类),然后把这个 Drawable对象设置为可见。Android没有提供回调或函数能让我们在每加载完一帧后都清理掉之前的图片,所以会出现 OOM的问题。
2. VideoView播放3gp文件:VideoView背景色适配问题。
同样的一段代码,在不同的手机上,可以看到 VideoView背景色值并不是整个 Activity的背景色。显示效果比较难看。
VideoView videoView = (VideoView) findViewById(R.id.video); videoView.setBackgroundColor(Color.TRANSPARENT); videoView.setVideoURI(Uri.parse("android.resource://com.example.android/" + R.raw.frames)); videoView.setMediaController(new MediaController(MainActivity.this)); videoView.start();
3. WebView加载 gif图片:图片模糊。
gif格式的图片最多只能有256种色值,如果把100张图片用 Flash导出成一整张 gif图片,会有很多小黑点,原因就是不在那256个基础色值的像素点都会显示成黑色。现在手机的屏幕尺寸、分辨率都越来越大,使用 gif格式的图片不能满足视觉要求。
4. WebView播放 flash:需要插件支持。
目前 Android和 Adobe搞不到一起,用户需要下载 Adobe Flash Player才能在手机上看.flv,H5的<video>标签也需要浏览器的 Flash插件支持。
5. 自定义控件逐帧加载图片:手机处理速度跟不上。
思路是这样的:以当前图片为中心向后预加载5帧(即维持缓存中最多保留5个 Bitmap对象,防止 OOM出现),比如当前是第20帧,那么缓存里应该有20-24这5帧的图片。然后每加载一帧,就在后台线程 recycle一张旧的 bitmap并 decode一张新的 bitmap。
但是.. BitmapFactory.decodeResource()执行速度跟不上...
100张图,2.5秒,即两帧间隔是25ms,在组里的十几个 Android手机上,最快的手机单线程执行BitmapFactory.decodeResource()方法也需要40ms左右,平均是70ms。比如现在要加载第6帧,这帧对应的 bitmap应该是加载完成第1张后开始 decode的,没法保证在第5帧跳转到第6帧的时候,这个 bitmap一定能够 decode完成。当然可以通过增加缓存的帧数和延长两帧间的间隔实现,但这么做并不保险,因为手机性能与多线程管理的差异,我们并不能确保 decode操作开始执行的时间与实际执行的时长,并且1秒24帧,是效果连续的最低要求。
这让我想到小时候做的脑残应用题——池塘里有两根水管,一个加水一个放水,放水的速度是加水的3倍...
以上是这5种方法遇到的问题,在帧数较少并且每一帧图片尺寸不大的情况下,AnimationDrawable是首选。安装有 Flash插件的情况下,可以把逐帧动画导出成一个.flv文件,播放效果好于.3gp文件或是 gif图片。
既然使用 Video可以加载视频,并且不出现 OOM,那么 Android系统上肯定有方法能够实现高清、连续的逐帧动画实现方法。