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();
}
}
上一篇: PHP实现桶排序算法实例分享