Android 提供了AnimationDrawable
用于實(shí)現(xiàn)幀動(dòng)畫纬纪。在動(dòng)畫開始之前,所有幀的圖片都被解析并占用內(nèi)存次和,一旦動(dòng)畫較復(fù)雜幀數(shù)較多,在低配置手機(jī)上容易發(fā)生 OOM那伐。即使不發(fā)生 OOM踏施,也會(huì)對內(nèi)存造成不小的壓力。下面代碼展示了一個(gè)幀數(shù)為4的幀動(dòng)畫:
AnimationDrawable drawable = new AnimationDrawable();
drawable.addFrame(getDrawable(R.drawable.frame1), frameDuration);
drawable.addFrame(getDrawable(R.drawable.frame2), frameDuration);
drawable.addFrame(getDrawable(R.drawable.frame3), frameDuration);
drawable.addFrame(getDrawable(R.drawable.frame4), frameDuration);
drawable.setOneShot(true);
ImageView ivFrameAnim = ((ImageView) findViewById(R.id.frame_anim));
ivFrameAnim.setImageDrawable(drawable);
drawable.start();
有沒有什么辦法讓幀動(dòng)畫的數(shù)據(jù)逐幀加載罕邀,而不是一次性全部加載到內(nèi)存畅形?SurfaceView就提供了這種能力。
SurfaceView
屏幕的顯示機(jī)制和幀動(dòng)畫類似诉探,也是一幀一幀的連環(huán)畫日熬,只不過刷新頻率很高,感覺像連續(xù)的肾胯。為了顯示一幀竖席,需要經(jīng)歷計(jì)算和渲染兩個(gè)過程,CPU 先計(jì)算出這一幀的圖像數(shù)據(jù)并寫入內(nèi)存敬肚,然后調(diào)用 OpenGL 命令將內(nèi)存中數(shù)據(jù)渲染成圖像存放在 GPU Buffer 中毕荐,顯示設(shè)備每隔一定時(shí)間從 Buffer 中獲取圖像并顯示。
上述過程中的計(jì)算艳馒,對于View來說东跪,就好比在主線程遍歷 View樹 以決定視圖畫多大(measure),畫在哪(layout)鹰溜,畫些啥(draw),計(jì)算結(jié)果存放在內(nèi)存中丁恭,SurfaceFlinger 會(huì)調(diào)用 OpenGL 命令將內(nèi)存中的數(shù)據(jù)渲染成圖像存放在 GPU Buffer 中曹动。每隔16.6ms,顯示器從 Buffer 中取出幀并顯示牲览。所以自定義 View 可以通過重載onMeasure()墓陈、onLayout()、onDraw()來定義幀內(nèi)容,但不能定義幀刷新頻率贡必。
SurfaceView可以突破這個(gè)限制兔港。而且它可以將計(jì)算幀數(shù)據(jù)放到獨(dú)立的線程中進(jìn)行。下面是自定義SurfaceView的模版代碼:
public abstract class BaseSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
public static final int DEFAULT_FRAME_DURATION_MILLISECOND = 50;
//用于計(jì)算幀數(shù)據(jù)的線程
private HandlerThread handlerThread;
private Handler handler;
//幀刷新頻率
private int frameDuration = DEFAULT_FRAME_DURATION_MILLISECOND;
//用于繪制幀的畫布
private Canvas canvas;
private boolean isAlive;
public BaseSurfaceView(Context context) {
super(context);
init();
}
protected void init() {
getHolder().addCallback(this);
//設(shè)置透明背景仔拟,否則SurfaceView背景是黑的
setBackgroundTransparent();
}
private void setBackgroundTransparent() {
getHolder().setFormat(PixelFormat.TRANSLUCENT);
setZOrderOnTop(true);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
isAlive = true;
startDrawThread();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
stopDrawThread();
isAlive = false;
}
//停止幀繪制線程
private void stopDrawThread() {
handlerThread.quit();
handler = null;
}
//啟動(dòng)幀繪制線程
private void startDrawThread() {
handlerThread = new HandlerThread(“SurfaceViewThread”);
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
handler.post(new DrawRunnable());
}
private class DrawRunnable implements Runnable {
@Override
public void run() {
if (!isAlive) {
return;
}
try {
//1.獲取畫布
canvas = getHolder().lockCanvas();
//2.繪制一幀
onFrameDraw(canvas);
} catch (Exception e) {
e.printStackTrace();
} finally {
//3.將幀數(shù)據(jù)提交
getHolder().unlockCanvasAndPost(canvas);
//4.一幀繪制結(jié)束
onFrameDrawFinish();
}
//不停的將自己推送到繪制線程的消息隊(duì)列以實(shí)現(xiàn)幀刷新
handler.postDelayed(this, frameDuration);
}
}
protected abstract void onFrameDrawFinish();
protected abstract void onFrameDraw(Canvas canvas);
}
用HandlerThread作為獨(dú)立幀繪制線程衫樊,好處是可以通過與其綁定的Handler方便地實(shí)現(xiàn)“每隔一段時(shí)間刷新”,而且在Surface被銷毀的時(shí)候可以方便的調(diào)用HandlerThread.quit()來結(jié)束線程執(zhí)行的邏輯利花。
DrawRunnable.run()運(yùn)用模版方法模式定義了繪制算法框架科侈,其中幀繪制邏輯的具體實(shí)現(xiàn)被定義成兩個(gè)抽象方法,推遲到子類中實(shí)現(xiàn)炒事,因?yàn)槔L制的東西是多樣的臀栈,對于本文來說,繪制的就是一張張圖片挠乳,所以新建BaseSurfaceView的子類FrameSurfaceView权薯。
逐幀解析 & 及時(shí)回收
public class FrameSurfaceView extends BaseSurfaceView {
public static final int INVALID_BITMAP_INDEX = Integer.MAX_VALUE;
private List bitmaps = new ArrayList<>();
//幀圖片
private Bitmap frameBitmap;
//幀索引
private int bitmapIndex = INVALID_BITMAP_INDEX;
private Paint paint = new Paint();
private BitmapFactory.Options options = new BitmapFactory.Options();
//幀圖片原始大小
private Rect srcRect;
//幀圖片目標(biāo)大小
private Rect dstRect = new Rect();
private int defaultWidth;
private int defaultHeight;
public void setDuration(int duration) {
int frameDuration = duration / bitmaps.size();
setFrameDuration(frameDuration);
}
public void setBitmaps(List bitmaps) {
if (bitmaps == null || bitmaps.size() == 0) {
return;
}
this.bitmaps = bitmaps;
//默認(rèn)情況下,計(jì)算第一幀圖片的原始大小
getBitmapDimension(bitmaps.get(0));
}
private void getBitmapDimension(Integer integer) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(this.getResources(), integer, options);
defaultWidth = options.outWidth;
defaultHeight = options.outHeight;
srcRect = new Rect(0, 0, defaultWidth, defaultHeight);
requestLayout();
}
public FrameSurfaceView(Context context) {
super(context);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
dstRect.set(0, 0, getWidth(), getHeight());
}
@Override
protected void onFrameDrawFinish() {
//在一幀繪制完后睡扬,直接回收它
recycleOneFrame();
}
//回收幀
private void recycleOneFrame() {
if (frameBitmap != null) {
frameBitmap.recycle();
frameBitmap = null;
}
}
@Override
protected void onFrameDraw(Canvas canvas) {
//繪制一幀前需要先清畫布盟蚣,否則所有幀都疊在一起同時(shí)顯示
clearCanvas(canvas);
if (!isStart()) {
return;
}
if (!isFinish()) {
drawOneFrame(canvas);
} else {
onFrameAnimationEnd();
}
}
//繪制一幀,是張Bitmap
private void drawOneFrame(Canvas canvas) {
frameBitmap = BitmapUtil.decodeOriginBitmap(getResources(),
bitmaps.get(bitmapIndex), options);
canvas.drawBitmap(frameBitmap, srcRect, dstRect, paint);
bitmapIndex++;
}
private void onFrameAnimationEnd() {
reset();
}
private void reset() {
bitmapIndex = INVALID_BITMAP_INDEX;
}
//幀動(dòng)畫是否結(jié)束
private boolean isFinish() {
return bitmapInde
x >= bitmaps.size();
}
//幀動(dòng)畫是否開始
private boolean isStart() {
return bitmapIndex != INVALID_BITMAP_INDEX;
_BITMAP_INDEX;
}
//幀動(dòng)畫是否結(jié)束
private boolean isFinish() {
return bitmapInde[外鏈圖片轉(zhuǎn)存中…(img-Eug6Luq9-1642137464297)]
x >= bitmaps.size();
}
//幀動(dòng)畫是否開始
private boolean isStart() {
return bitmapIndex != INVALID_BITMAP_INDEX;
}