Android系统本身提供了帧动画的实现:animation-list; 比如下面的:
xml
<?xml version="1.0" encoding="utf-8"?>
<!--FrameAnimator-->
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/z1"
android:duration="100" />
<item
android:drawable="@drawable/z2"
android:duration="100" />
<item
android:drawable="@drawable/z3"
android:duration="100" />
<!--...-->
</animation-list
这个动画使用场景在图片较小,帧数较小的场景下。 不适用于,帧数多,图片大的场景下使用,容易触发内存溢出(OOM)问题。
因此在大图片的帧动画实现必须自定义实现!
帧动画 实际是顺序图片集合按顺序播放,实现的放电影效果。
- 显示载体选用ImageView,并采用 SetBitmap的方式(实际使用发现在高频率设置ImageResource方法会导致UI界面不刷新的bug),并且通过Bitmap,也便于自己控制内存的消耗,解决OOM风险;
- 帧加载策略:注意不是完整加载所有帧,只创建待显示的帧和即将显示的帧。图片加载成Bitmap的格式待显示,并创建一个Bitmap的缓存集合,控制Bitmap的创建回收策略,控制内存不盲目扩张,导致APP运行OOM。
- 图片资源转Bitmap放在子线程处理,需要提前创建"用于生产Bitmap"的线程,且是单线程,避免动画过程中因线程动态创建耗时卡冻,影响动画流畅性;同时把创建Bitmap任务设置在子线程中,避免了主线程因超负荷加载图片导致的卡死风险。 提高了动画的容错率。
- 设置播放进度 实现动画进度自由控制。
完整流程编码如下:
Bitmap创建线程实现:
java
package com.cw.widget.fanim;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 作用:提前创建子线程 用来创建Bitmap,节约动态创建线程的耗时。
*
* @author liuwenbo
*/
public class BitmapCreator {
private static final String TAG = "BitmapCreator";
private static final Boolean isDebug = false;
private final ScheduledExecutorService executorService;
private static BitmapCreator instance = new BitmapCreator();
public static BitmapCreator getInstance() {
return instance;
}
private BitmapCreator() {
executorService = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder()
.namingPattern("createBitmap-schedule-pool-%d").daemon(true).build());
}
/**
* 子线程创建Bitmap。 最大耗时50ms, 不阻塞主线程
*
* @param context
* @param resId
* @return
*/
public Bitmap createBitmap(Context context, int resId) {
Bitmap bitmap = null;
long startTime = System.currentTimeMillis();
try {
Future<Bitmap> bitmapFuture = executorService.submit(() -> BitmapFactory.decodeResource(context.getResources(), resId));
bitmap = bitmapFuture.get(50, TimeUnit.MILLISECONDS);
Log.i(TAG, "isCancelled == " + bitmapFuture.isCancelled() + " -- isDone == " + bitmapFuture.isDone());
if (!bitmapFuture.isCancelled() && !bitmapFuture.isDone()) {
bitmapFuture.cancel(true);
}
} catch (Exception e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
long dTime = endTime - startTime;
if (isDebug) {
Log.e(TAG, "createBitmap use time == " + dTime);
}
return bitmap;
}
}
Bitmap帧管理类:
java
package com.cw.widget.fanim;
import android.graphics.Bitmap;
import android.util.Log;
import android.widget.ImageView;
import com.cw.widget.R;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* 帧专成Bitmap的方式设置图像。 防止SetImageRes不显示图像
*
* @author liuwenbo
*/
public class FrameBitmapManager {
private static final boolean isDebug = false;
private static final String TAG = "FrameBitmap";
private static final int CHECK_SIZE = 5;
private final List<Integer> sourceList;
private final ImageView frameImageView;
private HashMap<Integer, Bitmap> cacheMap = new HashMap<>();
public FrameBitmapManager(ImageView frameImageView, List<Integer> sourceList) {
this.sourceList = sourceList;
this.frameImageView = frameImageView;
BitmapCreator.getInstance();
}
public void setProgress(float progress) {
long startTime = System.currentTimeMillis();
int index = progressToIndex(progress);
int resId = sourceList.get(index);
Bitmap bitmap = getResourceBitmap(resId);
if (bitmap == null) {
Log.e(TAG, "bitmap is null, No set frame image");
return;
}
Object tagId = frameImageView.getTag(R.id.view_tag);
Object oldTagBitmap = frameImageView.getTag(R.id.cb_item_tag);
frameImageView.setImageBitmap(bitmap);
frameImageView.setTag(R.id.view_tag, resId);
frameImageView.setTag(R.id.cb_item_tag, bitmap);
if (tagId != null && tagId instanceof Integer && oldTagBitmap != null &&
oldTagBitmap instanceof Bitmap) {
if (!((Bitmap) oldTagBitmap).isRecycled()) {
setCacheData((Integer) tagId, (Bitmap) oldTagBitmap);
}
}
long endTime = System.currentTimeMillis();
if (isDebug) {
Log.e(TAG, "setProgress time == " + (endTime - startTime));
}
}
private void setCacheData(Integer key, Bitmap bitmap) {
if (cacheMap.size() > CHECK_SIZE) {
for (Iterator<Map.Entry<Integer, Bitmap>> it = cacheMap.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<Integer, Bitmap> item = it.next();
Bitmap map = item.getValue();
if (map != null && !map.isRecycled()) {
map.recycle();
}
it.remove();
break;
}
}
cacheMap.put(key, bitmap);
}
private int progressToIndex(float progress) {
int size = sourceList == null ? 0 : sourceList.size();
int index = (int) (progress * size);
if (index <= 0) {
return 0;
} else if (index > size - 1) {
return size - 1;
} else {
return index;
}
}
private Bitmap getResourceBitmap(int resId) {
Object value = cacheMap.remove(resId);
if (value != null) {
if (isDebug) {
Log.e(TAG, "use cache");
}
return (Bitmap) value;
} else {
return BitmapCreator.getInstance().createBitmap(frameImageView.getContext(), resId);
}
}
public void release() {
for (Iterator<Map.Entry<Integer, Bitmap>> it = cacheMap.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<Integer, Bitmap> item = it.next();
Bitmap map = item.getValue();
if (map != null && !map.isRecycled()) {
map.recycle();
}
it.remove();
}
}
}
应用在ImageView的实例:
kotlin
package com.cw.widget.fanim
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.AppCompatImageView
/**
* 帧动画View
*/
class FrameAnimImageView : AppCompatImageView {
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
}
private val frameRatio = 50;
private val msgGoNext = 1;
var isLooping = false;
private var isPlaying = false;
private var userPlayControl = false;
private var currentProcess: Int = 0;
private val frameList = ArrayList<Int>();
private val aminHelper = FrameBitmapManager(this, frameList);
private val playHandler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
if (msg.what == msgGoNext) {
var p = currentProcess + 1;
var isEnd = false
if (p > 100) {
if (isLooping) {
p = 0;
} else {
p = 100;
isEnd = true;
}
}
setProgress(p)
if (isPlaying && !isEnd) {
sendEmptyMessageDelayed(msgGoNext, 1000L / frameRatio)
}
}
}
}
fun setFrameResource(res: List<Int>) {
frameList.clear()
if (res.isNotEmpty()) {
frameList.addAll(res)
}
}
/**
* 0-100;
*/
fun setProgress(process: Int) {
currentProcess = process
aminHelper.setProgress(process / 100f)
}
fun start() {
userPlayControl = true;
play()
}
fun stop() {
userPlayControl = false
pause()
}
private fun play() {
isPlaying = true;
playHandler.sendEmptyMessage(msgGoNext)
}
private fun pause() {
isPlaying = false;
playHandler.removeMessages(msgGoNext)
}
private fun release() {
pause()
playHandler.removeCallbacksAndMessages(null)
aminHelper.release()
}
override fun onWindowVisibilityChanged(visibility: Int) {
super.onWindowVisibilityChanged(visibility)
if (visibility == View.VISIBLE && userPlayControl) {
play()
} else {
release()
}
}
}
此设计适用范围:帧图较多,较大,动画随用户可控,页面切换动画效果 实际效果展示: