Reuse Bitmap to realize high-resolution sequence frame animation

When playing high-resolution frame animations with a large number of frames, it is easy to use AnimationDrawable directly to OOM, because AnimationDrawable will load all animation pictures at once when inflate.
So take a different approach and use mutable Bitmap. Every time you render an animated picture, load the picture to the Bimap, and then render the Bitmap to the SurfaceView, and loop to the next animated picture every 1000/FPS ms until the animation is finished.

The following is the code implementation (the performance of the board at hand is weak and can only reach 20 FPS. Or use multiple mutable Bitmaps and multiple corresponding threads to load different animated pictures concurrently and render them to SurfaceView simultaneously to improve 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;
    }
}

Use arrays.xml to specify animation pictures

<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <array name="shutdown_drawables">
        <item>@drawable/shutdown_000</item>
        ...
        <item>@drawable/shutdown_199</item>
    </array>
</resources>

Guess you like

Origin blog.csdn.net/hegan2010/article/details/103992707