播放高分辨率的帧数多的帧动画时,直接使用 AnimationDrawable 容易OOM,因为 AnimationDrawable 会在 inflate 时一次性 load 所有动画图片。
因此另辟蹊径,使用 mutable Bitmap,每渲染一张动画图片,就把图片 load 到该 Bimap,然后把该 Bitmap 渲染到 SurfaceView 上,每 1000/FPS ms 后循环到下一张动画图片,直到动画播放完毕。
以下是代码实现(手头的板子性能较弱,只能达到 20 FPS。或者使用多个 mutable Bitmap 及多个对应的线程,并发加载不同动画图片,同步渲染到 SurfaceView,以提升 FPS。)
import android.app.Activity;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
public abstract class AnimationActivity extends Activity implements Runnable, AnimationListener,
SurfaceHolder.Callback {
private static final boolean DEBUG = false;
private static final float DRAW_FPS = 20f;
private static final long DRAW_PERIOD = (long) (1000 / DRAW_FPS);
protected final String mTag = getClass().getSimpleName();
private SurfaceView mSurfaceView;
private Bitmap mReusableBitmap;
private ScheduledExecutorService mScheduledExecutor;
private ScheduledFuture<?> mScheduledFuture;
private boolean mIsBoot;
private int[] mDrawableIds;
private int mDrawableIndex;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.v(mTag, "onCreate");
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else {
getWindow().getDecorView().setSystemUiVisibility(
/**5894**/View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
mSurfaceView = new SurfaceView(this);
mSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
mSurfaceView.getHolder().addCallback(this);
setContentView(mSurfaceView);
initAnimationDrawables();
AnimationController.getInstance().addAnimationListener(this);
}
protected abstract int getAnimationDrawableArrayId(boolean isBoot);
private void initAnimationDrawables() {
int drawableArrayId = getAnimationDrawableArrayId(mIsBoot);
TypedArray ar = getResources().obtainTypedArray(drawableArrayId);
int len = ar.length();
mDrawableIds = new int[len];
for (int i = 0; i < len; i++) {
int id = ar.getResourceId(i, 0);
mDrawableIds[i] = id;
}
ar.recycle();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.v(mTag, "surfaceCreated");
AnimationController.getInstance().notifyReadyToPlay(this);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.v(mTag, "surfaceChanged");
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.v(mTag, "surfaceDestroyed");
stopDraw();
}
private long mLastTime = 0;
private final StringBuilder mDrawInterval = DEBUG ? new StringBuilder() : null;
private final StringBuilder mDecodeTime = DEBUG ? new StringBuilder() : null;
@Override
public void run() {
if (DEBUG) {
long curTime = System.currentTimeMillis();
if (mDrawableIndex == 0) {
mDrawInterval.delete(0, mDrawInterval.length());
mDecodeTime.delete(0, mDecodeTime.length());
}
mDrawInterval.append(curTime - mLastTime).append(' ');
mLastTime = curTime;
}
long startTime = DEBUG ? System.currentTimeMillis() : 0;
mReusableBitmap = decodeSampledBitmapFromResources(getResources(),
mDrawableIds[mDrawableIndex++], mReusableBitmap);
if (DEBUG) {
mDecodeTime.append(System.currentTimeMillis() - startTime).append(' ');
}
Canvas canvas = mSurfaceView.getHolder().lockCanvas();
if (canvas != null) {
try {
canvas.drawBitmap(mReusableBitmap, 0, 0, null);
} catch (RuntimeException e) {
Log.e(mTag, "drawBitmap: " + e);
} finally {
mSurfaceView.getHolder().unlockCanvasAndPost(canvas);
}
}
if (mDrawableIndex == mDrawableIds.length) {
if (DEBUG) {
Log.w(mTag, mDrawInterval.toString());
Log.w(mTag, mDecodeTime.toString());
}
stopAnimation(false);
}
}
@Override
protected void onStart() {
super.onStart();
Log.v(mTag, "onStart");
}
@Override
protected void onPause() {
super.onPause();
Log.v(mTag, "onPause");
}
@Override
protected void onDestroy() {
Log.v(mTag, "onDestroy");
super.onDestroy();
if (mScheduledExecutor != null) {
mScheduledExecutor.shutdownNow();
}
AnimationController.getInstance().removeAnimationListener(this);
}
private void startDraw() {
Log.d(mTag, "startDraw");
mDrawableIndex = 0;
mScheduledExecutor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "DRAW-THREAD@" + mTag) {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
super.run();
}
};
}
});
mScheduledFuture = mScheduledExecutor.scheduleAtFixedRate(this, 0, DRAW_PERIOD,
TimeUnit.MILLISECONDS);
}
private void stopDraw() {
Log.d(mTag, "stopDraw");
if (mScheduledFuture == null) {
Log.w(mTag, "mScheduledFuture is null");
return;
}
mScheduledFuture.cancel(false);
mScheduledFuture = null;
}
@Override
public void startAnimation() {
Log.d(mTag, "startAnimation");
startDraw();
}
@Override
public void stopAnimation(boolean withNewAnimation) {
Log.d(mTag, "stopAnimation " + withNewAnimation);
stopDraw();
finish();
}
@Override
public void finish() {
super.finish();
Log.v(mTag, "finish");
}
/**
* Decode a bitmap from resources.
*
* @param res The resources to get drawable
* @param id The drawable resource id
* @param candidate The candidate bitmap to reuse
* @return A bitmap with the same dimensions that are equal to the candidate's width and height
*/
public static Bitmap decodeSampledBitmapFromResources(Resources res, int id, Bitmap candidate) {
if (candidate == null || candidate.isRecycled()) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
Bitmap bitmap = BitmapFactory.decodeResource(res, id, options);
Log.d("Bitmap", "bitmap.getConfig(): " + bitmap.getConfig());
return bitmap;
}
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, id, options);
if (canUseForInBitmap(candidate, options)) {
// inBitmap only works with mutable bitmaps so force the decoder to
// return mutable bitmaps.
options.inMutable = true;
options.inBitmap = candidate;
} else {
candidate.recycle();
}
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, id, options);
}
/**
* @param candidate Bitmap to check
* @param options Options that have the out* value populated
* @return true if candidate can be used for inBitmap re-use with options
*/
private static boolean canUseForInBitmap(Bitmap candidate, BitmapFactory.Options options) {
// From Android 4.4 (KitKat) onward we can re-use if the byte size of the new bitmap
// is smaller than the reusable bitmap candidate allocation byte count.
int byteCount = options.outWidth * options.outHeight
* getBytesPerPixel(candidate.getConfig());
return byteCount <= candidate.getAllocationByteCount();
}
/**
* Return the byte usage per pixel of a bitmap based on its configuration.
*
* @param config The bitmap configuration.
* @return The byte usage per pixel.
*/
private static int getBytesPerPixel(Bitmap.Config config) {
if (config == Bitmap.Config.ARGB_8888) {
return 4;
} else if (config == Bitmap.Config.RGB_565) {
return 2;
} else if (config == Bitmap.Config.ARGB_4444) {
return 2;
} else if (config == Bitmap.Config.ALPHA_8) {
return 1;
}
return 1;
}
}
使用 arrays.xml 来指定动画图片
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<array name="shutdown_drawables">
<item>@drawable/shutdown_000</item>
...
<item>@drawable/shutdown_199</item>
</array>
</resources>