复用Bitmap实现高分辨率序列帧动画

播放高分辨率的帧数多的帧动画时,直接使用 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>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值