Android 序列帧动画

这是我发表的第一篇文章,如有不对之处,请大家多多指教。
最近项目有个需求,做一个序列帧动画的播放(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();
    }
}

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值