前言
逐幀動(dòng)畫 (Frame By Frame) 是 Android 系統(tǒng)提供的一種常見(jiàn)的動(dòng)畫形式嘲更,通過(guò)播放一組連續(xù)的圖片資源形成動(dòng)畫冰木。當(dāng)我們想用一組連續(xù)的圖片播放動(dòng)畫時(shí),首先想到的就是使用系統(tǒng)提供的逐幀動(dòng)畫方式。接下來(lái)超升,我們將簡(jiǎn)單說(shuō)明如何使用逐幀動(dòng)畫嗓违,以及分析逐幀動(dòng)畫存在的優(yōu)缺點(diǎn)九巡,最后給出我們的解決方案。
逐幀動(dòng)畫
- 第一步蹂季,將我們所需要的動(dòng)畫素材資源放置在 res/drawable 目錄下冕广,切記不要因?yàn)槭莿?dòng)畫所以就錯(cuò)誤的將素材資源放置在 res/anim 目錄下。
-
第二步乏盐,在 res/anim 目錄下新建 drawable 文件 loading.xml ,如下
animation-list 為 drawable 文件的根標(biāo)簽佳窑,android:oneshot 設(shè)置動(dòng)畫是否只播放一次,子標(biāo)簽 item 具體定義每一幀的動(dòng)畫父能,android:drawable 定義這一幀動(dòng)畫所使用的資源神凑,android:duration 設(shè)置動(dòng)畫的持續(xù)時(shí)間。
-
第三步何吝,給想要顯示動(dòng)畫的 ImageView 設(shè)置資源動(dòng)畫溉委,然后開啟動(dòng)畫
我們能看到,逐幀動(dòng)畫使用起來(lái)是如此的簡(jiǎn)單方便爱榕,所以當(dāng)我們想要通過(guò)一組圖片素材來(lái)實(shí)現(xiàn)動(dòng)畫的時(shí)候首選的就是以上的方案瓣喊。但是我們卻忽略了一個(gè)情況,當(dāng)圖片素材很多并且每張圖片都很大的情況下黔酥,使用以上的方法手機(jī)會(huì)出現(xiàn) OOM 以及卡頓問(wèn)題藻三,這是幀動(dòng)畫的一個(gè)比較明顯的缺點(diǎn)洪橘。
為什么幀動(dòng)畫會(huì)出現(xiàn) OOM 以及卡頓?
我們知道棵帽,在第三步給 ImageView 設(shè)置圖片資源的時(shí)候熄求,因?yàn)?loading.xml 文件中定義了一系列的圖片素材,系統(tǒng)會(huì)按照每個(gè)定義的順序把所有的圖片都讀取到內(nèi)存中逗概,而系統(tǒng)讀取圖片的方式是 Bitmap 位圖形式弟晚,所以就導(dǎo)致了 OOM 的發(fā)生。
解決方案
既然一次性讀取所有的圖片資源會(huì)導(dǎo)致內(nèi)存溢出逾苫,那么我們能想到的解決方法就是按照動(dòng)畫的順序卿城,每次只讀取一幀動(dòng)畫資源,讀取完畢再顯示出來(lái)铅搓,如果圖片過(guò)大瑟押,我們還需要對(duì)圖片進(jìn)行壓縮處理。
技術(shù)實(shí)現(xiàn)
總體思路是這樣的星掰,我們?cè)谧泳€程里讀取圖片資源(包括圖片過(guò)大勉耀,對(duì)圖片進(jìn)行處理),讀取完畢后通過(guò)主線程的 Handler 將在子線程的數(shù)據(jù)(主要是 Bitmap)發(fā)送到主線程中蹋偏,然后再把 Bitmp 繪制顯示出來(lái)便斥,每隔一段時(shí)間不斷讀取,然后依次顯示出來(lái)威始,這樣視覺(jué)上就有了動(dòng)畫的效果枢纠。實(shí)現(xiàn)代碼如下
public class AnimationView extends View implements Handler.Callback {
public static final int DEFAULT_ANIM_TIME = 100;
public static final int PROCESS_DATA = 1;
public static final int PROCESS_ANIM_FINISH = 1 << 1;
public static final int PROCESS_DELAY = 1 << 2;
public AnimData mCurAnimData;
public int mCurAnimPos;
public boolean mIsRepeat;
public int mAnimTime;
private Handler mHandler ;
private ProcessAnimThread mProcessThread;
private Bitmap mCurShowBmp;
private List<AnimData> mAnimDataList = new ArrayList<>();
public AnimationView(Context context) {
this(context,null);
}
public AnimationView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public AnimationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
mHandler = new Handler(this);
mProcessThread = new ProcessAnimThread(getContext(),mHandler);
mAnimTime = DEFAULT_ANIM_TIME;
}
public void setIsRepeat(boolean repeat){
mIsRepeat = repeat;
}
private int mGravity;
public void SetGravity(int gravity)
{
mGravity = gravity;
invalidate();
}
public void setData(List<AnimData> list){
if (list != null ){
mAnimDataList.addAll(list);
}
}
private Matrix mTempMatrix = new Matrix();
@Override
protected void onDraw(Canvas canvas) {
if(mCurShowBmp != null && !mCurShowBmp.isRecycled())
{
int x = 0;
int y = 0;
float scaleX = 1f;
float scaleY = 1f;
switch(mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)
{
case Gravity.LEFT:
x = 0;
break;
case Gravity.RIGHT:
x = this.getWidth() - mCurShowBmp.getWidth();
break;
case Gravity.CENTER_HORIZONTAL:
x = (this.getWidth() - mCurShowBmp.getWidth()) / 2;
break;
case Gravity.FILL_HORIZONTAL:
{
int w = mCurShowBmp.getWidth();
if(w > 0)
{
scaleX = (float)this.getWidth() / (float)w;
}
break;
}
default:
break;
}
switch(mGravity & Gravity.VERTICAL_GRAVITY_MASK)
{
case Gravity.TOP:
y = 0;
break;
case Gravity.BOTTOM:
y = this.getHeight() - mCurShowBmp.getHeight();
break;
case Gravity.CENTER_VERTICAL:
y = (this.getHeight() - mCurShowBmp.getHeight()) / 2;
break;
case Gravity.FILL_VERTICAL:
{
int h = mCurShowBmp.getHeight();
if(h > 0)
{
scaleY = (float)this.getHeight() / (float)h;
}
break;
}
default:
break;
}
if(scaleX == 1 && scaleY != 1)
{
scaleX = scaleY;
switch(mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)
{
case Gravity.RIGHT:
x = this.getWidth() - (int)(mCurShowBmp.getWidth() * scaleX);
break;
case Gravity.CENTER_HORIZONTAL:
x = (this.getWidth() - (int)(mCurShowBmp.getWidth() * scaleX)) / 2;
break;
}
}
else if(scaleX != 1 && scaleY == 1)
{
scaleY = scaleX;
switch(mGravity & Gravity.VERTICAL_GRAVITY_MASK)
{
case Gravity.BOTTOM:
y = this.getHeight() - (int)(mCurShowBmp.getHeight() * scaleY);
break;
case Gravity.CENTER_VERTICAL:
y = (this.getHeight() - (int)(mCurShowBmp.getHeight() * scaleY)) / 2;
break;
}
}
mTempMatrix.reset();
mTempMatrix.postScale(scaleX, scaleY);
mTempMatrix.postTranslate(x, y);
canvas.drawBitmap(mCurShowBmp, mTempMatrix, null);
}
}
private boolean mHasStarted = false;
public void start(){
mHasStarted = true;
if (mWidth == 0 || mHeight == 0 ){
return;
}
startPlay();
}
private void startPlay() {
if ( mAnimDataList != null && mAnimDataList.size() > 0 ){
mCurAnimPos = 0;
AnimData animData = mAnimDataList.get(mCurAnimPos);
mCurShowBmp = ImageUtil.getBitmap(getContext(),animData.filePath,mWidth,mHeight);
invalidate();
if (mListener != null ){
mListener.onAnimChange(mCurAnimPos,mCurShowBmp);
}
checkIsPlayNext();
}
}
private void playNext(final int curAnimPosition ){
Message msg = Message.obtain();
msg.what = PROCESS_DELAY;
msg.arg1 = curAnimPosition;
mHandler.sendMessageDelayed(msg,mAnimTime);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
quit();
}
private void quit(){
mHasStarted = false;
if (mProcessThread != null ){
mProcessThread.clearAll();
}
}
private int mWidth;
private int mHeight;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
if (mProcessThread != null ){
mProcessThread.setSize(w,h);
}
if (mHasStarted){
startPlay();
}
}
private boolean mHavePause = false;
public void pause(){
mHavePause = true;
mHandler.removeMessages(PROCESS_DELAY);
}
public void resume(){
if (mHavePause && mHasStarted){
checkIsPlayNext();
}
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what){
case PROCESS_ANIM_FINISH:{
Bitmap bitmap = (Bitmap) msg.obj;
if (bitmap != null){
if (mCurShowBmp != null ){
mCurShowBmp.recycle();
mCurShowBmp = null;
}
mCurShowBmp = bitmap;
if (mListener != null ){
mListener.onAnimChange(mCurAnimPos,bitmap);
}
invalidate();
}
checkIsPlayNext();
break;
}
case PROCESS_DELAY:{
int curAnimPosition = msg.arg1;
AnimData data = mAnimDataList.get(curAnimPosition);
mProcessThread.processData(data);
break;
}
}
return true;
}
private void checkIsPlayNext() {
mCurAnimPos ++;
if ( mCurAnimPos >= mAnimDataList.size() ){
if (mIsRepeat){
mCurAnimPos = 0;
playNext(mCurAnimPos);
} else {
if ( mListener != null ){
mListener.onAnimEnd();
}
}
} else {
playNext(mCurAnimPos);
}
}
private AnimCallBack mListener;
public void setAnimCallBack(AnimCallBack callBack){
mListener = callBack;
}
public interface AnimCallBack{
void onAnimChange(int position, Bitmap bitmap);
void onAnimEnd();
}
public static class AnimData{
public Object filePath;
}
public static class ProcessAnimThread{
private HandlerThread mHandlerThread;
private Handler mProcessHandler;
private Handler mUiHandler;
private AnimData mCurAnimData;
private int mWidth;
private int mHeight;
private WeakReference<Context> mContext;
public ProcessAnimThread(Context context, Handler handler){
mUiHandler = handler;
mContext = new WeakReference<Context>(context);
init();
}
public void setSize(int width,int height){
mWidth = width;
mHeight = height;
}
private void init(){
mHandlerThread = new HandlerThread("process_anim_thread");
mHandlerThread.start();
mProcessHandler = new Handler(mHandlerThread.getLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
// 消息是在子線程 HandlerThread 里面被處理,所以這里的 handleMessage 在
//子線程里被調(diào)用
switch (msg.what){
case PROCESS_DATA:{
AnimData animData = (AnimData) msg.obj;
Bitmap bitmap = ImageUtil.getBitmap(mContext.get(),animData.filePath,mWidth,mHeight);
if (bitmap != null ){
Message finishMsg = Message.obtain();
finishMsg.what = PROCESS_ANIM_FINISH;
finishMsg.obj = bitmap;
//消息處理完畢黎棠,使用主線程的 Handler 將消息發(fā)送到主線程
mUiHandler.sendMessage(finishMsg);
}
break;
}
}
return true;
}
});
}
public void processData(AnimData animData){
if ( animData != null ){
Message msg = Message.obtain();
msg.what = PROCESS_DATA;
msg.obj = animData;
mProcessHandler.sendMessage(msg);
}
}
public void clearAll(){
mHandlerThread.quit();
mHandlerThread = null;
}
}
}
-
首先定義靜態(tài)的內(nèi)部類 AnimData晋渺,作為我們的動(dòng)畫實(shí)體類,filePath 為動(dòng)畫的路徑脓斩,可以是 res 資源目錄下木西,也可以是 外部存儲(chǔ)的路徑。
-
接下來(lái)定義封裝 ProcessAnimThread 類随静,用以將資源圖片讀取為 Bitmap,如果圖片過(guò)大八千,我們還需要將其壓縮處理。ProcessAnimThread 類中燎猛,最為關(guān)鍵的是 HandlerThread 恋捆,這是自帶有 Looper 的 Thread,繼承自 Thread重绷。前面我們說(shuō)過(guò)在子線程里讀取 Bitmap, HandlerThread 就是我們上面提及的子線程沸停,使用方法上,我們先構(gòu)造 HandlerThread 昭卓,然后調(diào)用 start() 方法開啟線程愤钾,這時(shí)候 HandlerThread 里的 Looper 已經(jīng)啟動(dòng)可以出來(lái)消息了瘟滨,最后通過(guò)這個(gè) Looper 構(gòu)造 Handler(例子中為 mProcessHandler 變量)。完成以上步驟之后能颁,我們通過(guò) mProcessHandler 發(fā)送的消息最終會(huì)在 子線程里被處理室奏,處理完畢之后,再講結(jié)果發(fā)送到主線程
-
接下來(lái)看主線程收到消息后如何處理劲装。首先將結(jié)果取出來(lái),然后刷新顯示昌简,接著判斷隊(duì)列是否以及處理結(jié)束占业,未結(jié)束則通過(guò)發(fā)送延遲的消息繼續(xù)讀取圖片。
AnimationView 使用步驟
-
構(gòu)造幀動(dòng)畫數(shù)據(jù)隊(duì)列
調(diào)用 AnimationView 的 start() 方法開啟動(dòng)畫
寫在最后
AnimationView 是解決方案里的一個(gè)簡(jiǎn)單實(shí)現(xiàn)纯赎,由于知識(shí)水平有限谦疾,難免有錯(cuò)誤和遺漏,歡迎指正犬金。
最后念恍,附上項(xiàng)目地址 https://github.com/hanilala/CoolCode