欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android 序列帧动画

程序员文章站 2022-03-25 21:27:14
...

这是我发表的第一篇文章,如有不对之处,请大家多多指教。
最近项目有个需求,做一个序列帧动画的播放(3600张图片的播放,这么多图片,对内存,对cpu是一个考验)。我在项目的开发过程中,探索了很多方法,思路,下面我会一一介绍。

1.Android 原生方法

Android 原生方法适用于:图片小(分辨率小) 、内存小。这种方法会一次加载所有图片,对于序列帧图片大,并且在数量多的情况下,手机cpu与内存占用比较高,容易引起OOM。使用方法如下:

1.创建animation-list资源

在drawable资源下创建一个名为“wel.xml”的资源文件,代码如下

<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="true">
    <item android:duration="30" android:drawable="@drawable/m_00000" />
    <item android:duration="30" android:drawable="@drawable/m_00001" />
    <item android:duration="30" android:drawable="@drawable/m_00002" />
    <item android:duration="30" android:drawable="@drawable/m_00003" />
    <item android:duration="30" android:drawable="@drawable/m_00004" />
    <item android:duration="30" android:drawable="@drawable/m_00005" />
    <item android:duration="30" android:drawable="@drawable/m_00006" />
    <item android:duration="30" android:drawable="@drawable/m_00007" />
    <item android:duration="30" android:drawable="@drawable/m_00008" />
  </animation-list>

其中duration代表帧数 , oneshot代表是否循环播放,true为只播放一次,可以动态设置。

2.java代码中应用

image = findViewById(R.id.image);           // 布局文件需要提前创建一个imageView
image.setImageResource(R.drawable.wel);   
anim = (AnimationDrawable) image1.getDrawable();  //创建全局变量  AnimationDrawable anim;
anim.setOneShot(true);     //true-播放一次,结束后停留在最后一帧, false为循环播发

anim.start();         //开始播发
anim.stop();          //停止播发
anim.isRunning()      //是否播放
anim.getCurrent()     //当前播放帧数    返回的是一个drawable资源
anim.getNumberOfFrames()    // 序列帧总数
anim.getFrame(anim.getNumberOfFrames() -1)   //得到指定index 的drawable

2.SurfaceView播放序列帧

代码如***释的很清楚。注意的地方:设置30ms一张,实际会达不到这个效果,因为图片从png,或者jpeg格式转成png也是需要一部分时间的。
这个方法我借鉴了网上一位大神的写法,他给我提供了一些思路,链接如下:

https://blog.csdn.net/flowerff/article/details/83758695

public class SurfaceViewAnimation extends SurfaceView implements Runnable {
    private String TAG = "SurfaceViewAnimation";
    private SurfaceHolder mSurfaceHolder;
    private boolean mIsRunning = true; // 是否播发
    private int totalCount;//序列帧总数
    private Canvas mCanvas;
    private Bitmap mBitmap;//当前显示的图片
    private int mCurrentIndext;// 当前动画播放的位置
    private int mDuration = 30;// 每帧动画持续存在的时间
    public static boolean mIsDestroy = false;// 是否已经销毁
    private int[] mResourceIds;              // 图片资源id数组
    private ArrayList<String> mResourcePaths;// 图片资源path数组
    private OnAnimationListener mListener;// 动画监听事件
    private Thread thread;
    Rect mSrcRect, mDestRect;

    public SurfaceViewAnimation(Context context) {
        this(context, null);
        initView();
    }

    public SurfaceViewAnimation(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView();
    }

    public SurfaceViewAnimation(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        initView();
    }

    private void initView() {
        mSurfaceHolder = this.getHolder();
        mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_HARDWARE);
        mResourceIds = new int[1];
    }

    /**
     * 制图方法
     */
    private void drawView() {
        if (mResourceIds == null && mResourcePaths == null) {
            Log.e(TAG, "resource is null");
            mIsRunning = false;
            return;
        }
        Log.d(TAG, "drawView: Thread id = " + Thread.currentThread().getId());
        SurfaceHolder surfaceHolder = mSurfaceHolder;
        // 锁定画布
        synchronized (surfaceHolder) {
            if (surfaceHolder != null) {
                mCanvas = surfaceHolder.lockCanvas();
                Log.d(TAG, "drawView: mCanvas= " + mCanvas);
                if (mCanvas == null) {
                    return;
                }
            }
            try {
                if (surfaceHolder != null && mCanvas != null) {
                    synchronized (mResourceIds) {
                        if (mResourceIds != null && mResourceIds.length > 0) {
                            mBitmap = BitmapUtil.decodeSampledBitmapFromResource(getResources(), mResourceIds[mCurrentIndext], getWidth(), getHeight());
                        } else if (mResourcePaths != null && mResourcePaths.size() > 0) {
                            mBitmap = BitmapFactory.decodeFile(mResourcePaths.get(mCurrentIndext));
                        }
                    }
                    mBitmap.setHasAlpha(true);
                    if (mBitmap == null) {
                        return;
                    }
                    Paint paint = new Paint();
                    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
                    mCanvas.drawPaint(paint);
                    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
                    paint.setAntiAlias(true);
                    paint.setStyle(Paint.Style.STROKE);
                    mSrcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
                    mDestRect = new Rect(0, 0, getWidth(), getHeight());
                    mCanvas.drawBitmap(mBitmap, mSrcRect, mDestRect, paint);
                    // 播放到最后一张图片
                    if (mCurrentIndext == totalCount - 1) {
                        mIsRunning = false; //停止播发
                        //设置重复播放
                        //播放到最后一张,当前index置零
                        //mCurrentIndext = 0;
                    }
                }
            } catch (Exception e) {
                Log.d(TAG, "drawView: e =" + e.toString());
                e.printStackTrace();
            } finally {
                mCurrentIndext++;
                if (mCurrentIndext >= totalCount) {
                    mCurrentIndext = 0;
                }
                if (mCanvas != null) {
                    // 将画布解锁并显示在屏幕上
                    if (getHolder() != null) {
                        surfaceHolder.unlockCanvasAndPost(mCanvas);
                    }
                }
                if (mBitmap != null) {
                    //recycle通知底层(c++)回收,java有自己的回收机制  GC
                    mBitmap.recycle();
                }
            }
        }
    }

    @Override
    public void run() {
        if (mListener != null) {
            mListener.onStart();
        }
        while (mIsRunning) {
            drawView();
            try {
                Thread.sleep(mDuration);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (mListener != null) {
            mListener.onStop();
        }
    }

    /**
     * 开始播放
     */
    public void start() {
        if (!mIsDestroy) {
            mCurrentIndext = 0;
            mIsRunning = true;
            thread = new Thread(this);
            thread.start();
        } else {
            // 如果SurfaceHolder已经销毁抛出该异常
            try {
                throw new Exception("IllegalArgumentException:Are you sure the SurfaceHolder is not destroyed");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 防止内存泄漏, SurfaceHolder销毁时添加回调
     */
    private void destroy() {
        mIsRunning = false;
        try {
            Thread.sleep(mDuration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mIsDestroy = true;
        thread.interrupt();
        thread = null;
        if (mBitmap != null) {
            mBitmap.recycle();
            mBitmap = null;
        }

        if (mSurfaceHolder != null) {
            mSurfaceHolder.addCallback(null);
        }

        if (mListener != null) {
            mListener = null;
        }
    }

    /**
     * 设置动画播放素材的id,图片保存在res资源下
     *
     * @param ints 图片资源id
     */
    public void setResourceId(int[] ints) {
        synchronized (mResourceIds) {
            this.mResourceIds = ints;
            totalCount = ints.length;
        }
    }

    /**
     * 设置动画播放素材的路径,图片保存在车机本地
     *
     * @param resourcePath
     */
    public void setResourcePath(ArrayList resourcePath) {
        this.mResourcePaths = resourcePath;
        totalCount = resourcePath.size();
    }

    /**
     * 设置每帧时间
     */
    public void setDuration(int duration) {
        this.mDuration = duration;
    }

    /**
     * 停止播发
     */
    public void stop() {
        mIsRunning = false;
    }

    /**
     * 继续动画
     */
    public void reStart() {
        mIsRunning = false;
    }

    /**
     * 设置动画监听器
     */
    public void setListener(OnAnimationListener listener) {
        mListener = listener;
    }

    /**
     * 动画监听器
     *
     * @author qike
     */
    public interface OnAnimationListener {

        /**
         * 动画开始
         */
        void onStart();

        /**
         * 动画结束
         */
        void onStop();
    }
}

3.自定义Animation(!!!)

** 通过surfaceView的研究,发现动画其实就是一串连续的图片的切换。我们平时的图片资源是png,或者jpeg格式的,当使用imageView设置resource资源时,原理都是将图片转成bitMap格式,然后进行绘制,当图片量少,分辨率小使用一般的方法即可,但是当图片分辨率大,量大,这个时候播放的时间往往是图片转换的时间 + 我们播放的帧时间 > 理想时间。
**
** 于是我有一个想法,我可以一次性将自己所播放的图片资源转成bitMap资源,然后开启一个线程,指定时间,将imageView替换一次资源。可这样子会发现当图片过多的时候,内存占用过大,这对于手机而言,是不由好的,于是我想到分布加载,播放结束后,释放内存的方法,代码如下 **

/**
*图片加载类
*/
public class AnimationsContainer {
    private static final String TAG = "AnimationsContainer";
    private static AnimationsContainer mInstance;
    public static List<Bitmap> bitMapList = new ArrayList<>(); //我图片较多,所以我200为一轮加载,内存中永远占用0-199张资源,时刻准备加载
    private OnAnimationStoppedListener listener;
    private AnimationsContainer() {
        if (bitMapList .size() == 0) {
            getBit();
        }
    }

    /**
     * Get instance of AnimationsContainer.
     * @return AnimationsContainer.
     */
    public static AnimationsContainer getInstance() {
        if (mInstance == null) {
            mInstance = new AnimationsContainer();
        }
        return mInstance;
    }

    private void getBit() {
    //第一轮加载,0-199,这部分不会释放,一直保存
        new Thread(() -> {
            try {
                Log.d(TAG, "Read data for the first time ");
                initList(Constant.WEL_L, bitMapList , 1);
                listener.readSuccess();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }).start();
    }

    private void initList(String name, List<Bitmap> list, int index) {
        loop(name, list, index);
    }
    
	/*
	*加载bitMap的接口,name代表path,index代表第几轮加载
	*/
    public void getList(String name, List<Bitmap> list, int index) {
        Log.d(TAG, "getList: Load data " + index);
        new Thread(() -> loop(name, list, index)).start();
    }

    private void loop(String name, List<Bitmap> list, int index) {
        num = (index - 1) * 200;  //我设置的是200为一轮加载
        sum = 200 * index;
        for (int i = num; i < sum; i++) {
            String str;
            if (i < Constant.TEN) {
                str = "0000" + i;
            } else if (i < Constant.ONE_HUNDRED) {
                str = "000" + i;
            } else {
                str = "00" + i;
            }
            //Constant.PATH  我图片资源保存在手机的路径     Constant.BMP  图片的后缀 .png或者其他格式
            String path = Constant.PATH + name + str + Constant.BMP;
            FileInputStream fs = null;
            try {
                fs = new FileInputStream(path);
            } catch (FileNotFoundException ex) {
                ex.printStackTrace();
                Log.i(TAG, "file:" + path + "  is error");
            }
            Bitmap bitmap = BitmapFactory.decodeStream(fs);
            try {
                fs.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            list.add(bitmap);
        }
    }

    /**
     * Set OnAnimStopListener
     *
     * @param listener set listener.
     */
    public void setOnAnimStopListener(OnAnimationStoppedListener listener) {
        this.listener = listener;
    }
}
/*
*播放动画的类
*/
public class FramesSequenceAnimation {
    private static final String TAG = "FramesSequenceAnimation";
    private int mIndex = 0; // 当前帧
    private boolean mShouldRun; // 开始/停止播放用
    private boolean mIsRunning; // 动画是否正在播放,防止重复播放
    private SoftReference<ImageView> mSoftReferenceImageView; // 软引用ImageView,以便及时释放掉
    private final Handler mHandler;
    private static final int M_DELAY_MILLIS = 30;
    List<Bitmap> imageList = new ArrayList<>();
    List<Bitmap> imageList2 = new ArrayList<>();
    List<Bitmap> imageList3 = new ArrayList<>();
    private OnAnimationStoppedListener listener;
    private int mLever = 0;
    private AnimationsContainer animationsContainer;
    private String name = "";   //资源路径

    /**
     *
     * @param handler.
     * @param imageView.
     */
    public FramesSequenceAnimation(Handler handler, ImageView imageView) {
        mHandler = handler;
        mIndex = -1;
        mSoftReferenceImageView = new SoftReference<>(imageView);
        mShouldRun = false;
        mIsRunning = false;
        animationsContainer = AnimationsContainer.getInstance();
    }

    //循环读取下一帧
    private int getNext() {
        mIndex++;
        //设置只播放一次动画
        int size = 0;
        if (mIndex > 199) {
            mLever ++;
            Log.i(TAG, "mLever = " + mLever);
            mIndex = 0;
            if (mLever == 1) {
                Log.i(TAG, "mLever = 1");
                //开始播放第二轮,加载第三轮
                animationsContainer.getList(name, imageList3, 3);
            }
            if (mLever == Constant.THREE) {
                Log.i(TAG, "mLever = 3");
                stop();
            }
        }
        return mIndex;
    }

    /**
     * 播发.
     */
    public synchronized void start(String name) {
        mShouldRun = true;
        this.name = name;
        Log.d(TAG, "video start: name :" + name);
        //开始播放第一轮,加载第二轮
        animationsContainer.getList(name, imageList2, 2);
        if (mIsRunning) {
            return;
        }
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                ImageView imageView = mSoftReferenceImageView.get();
                if (!mShouldRun || imageView == null) {
                    mIsRunning = false;
                    if (listener != null) {
                        listener.stop();
                    }
                    return;
                }
                mIsRunning = true;
                mHandler.postDelayed(this, M_DELAY_MILLIS);
                if (imageView.isShown()) {
                    int imageRes = getNext();
                    switch (mLever) {
                        case 0 :
                            if (imageRes >= imageList.size()) {
                                Log.i(TAG, "imageList have not prepared");
                                return;
                            }
                            imageView.setImageBitmap(imageList.get(imageRes));
                            break;
                        case 1 :
                            if (imageRes >= imageList2.size()) {
                                Log.i(TAG, "imageList2 have not prepared");
                                return;
                            }
                            imageView.setImageBitmap(imageList2.get(imageRes));
                            break;
                        case Constant.TWO :
                            if (imageRes >= imageList3.size()) {
                                Log.i(TAG, "imageList3 have not prepared");
                                return;
                            }
                            imageView.setImageBitmap(imageList3.get(imageRes));
                            break;
                        default:
                            stop();
                            Log.d(TAG, "mLever is highest");
                    }
                }
            }
        };
        mHandler.post(runnable);
    }

    /**
     * 结束.
     */
    public synchronized void stop() {
        Log.d(TAG, "video stop");
        mLever = 0;
        mShouldRun = false;
        destroy(imageList2);
        destroy(imageList3);
        imageList2.clear();
        imageList3.clear();
        Log.d(TAG, "stop: clear success");
    }

    public void setImageList(List<Bitmap> imageList) {
        this.imageList = imageList;
    }

    public void setListener(OnAnimationStoppedListener listener) {
        this.listener = listener;
    }

   /*
   * 播放结束时的回调
   */
    private void destroy(List<Bitmap> list) {
        Log.d(TAG, "destroy: Recycle data");
        Bitmap bitmap = null;
        for (int i = 0; i < list.size(); i++) {
            bitmap = list.get(i);
            if (bitmap != null && !bitmap.isRecycled()) {
                bitmap.recycle();  //方法二中有介绍
                bitmap = null;
            }
        }
        System.gc();
    }
}