解決 Android 逐幀動(dòng)畫Drawable Animation 引起的OOM以及卡頓問(wèn)題

前言

逐幀動(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市晚顷,隨后出現(xiàn)的幾起案子峰伙,更是在濱河造成了極大的恐慌,老刑警劉巖该默,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞳氓,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡栓袖,警方通過(guò)查閱死者的電腦和手機(jī)匣摘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)裹刮,“玉大人音榜,你說(shuō)我怎么就攤上這事∨跗” “怎么了赠叼?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)违霞。 經(jīng)常有香客問(wèn)我梅割,道長(zhǎng),這世上最難降的妖魔是什么葛家? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任户辞,我火速辦了婚禮低飒,結(jié)果婚禮上构哺,老公的妹妹穿的比我還像新娘梗搅。我一直安慰自己社裆,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布双仍。 她就那樣靜靜地躺著枢希,像睡著了一般。 火紅的嫁衣襯著肌膚如雪朱沃。 梳的紋絲不亂的頭發(fā)上苞轿,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音逗物,去河邊找鬼搬卒。 笑死,一個(gè)胖子當(dāng)著我的面吹牛翎卓,可吹牛的內(nèi)容都是我干的契邀。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼失暴,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼坯门!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起逗扒,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤古戴,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后矩肩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體允瞧,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年蛮拔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了述暂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡建炫,死狀恐怖畦韭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肛跌,我是刑警寧澤艺配,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站衍慎,受9級(jí)特大地震影響转唉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜稳捆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一赠法、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乔夯,春花似錦砖织、人聲如沸款侵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)新锈。三九已至,卻和暖如春眶熬,著一層夾襖步出監(jiān)牢的瞬間妹笆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工娜氏, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拳缠,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓牍白,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親抖棘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子茂腥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,107評(píng)論 25 707
  • java 接口的意義-百度 規(guī)范、擴(kuò)展切省、回調(diào) 抽象類的意義-樂(lè)視 為其子類提供一個(gè)公共的類型封裝子類中得重復(fù)內(nèi)容定...
    交流電1582閱讀 2,228評(píng)論 0 11
  • 前幾天整理了Java面試題集合,今天再來(lái)整理下Android相關(guān)的面試題集合.如果你希望能得到最新的消息,可以關(guān)注...
    Boyko閱讀 3,632評(píng)論 8 135
  • 整本書用平實(shí)易懂的文字將現(xiàn)實(shí)之下那些赤裸裸的腐敗黑暗全部一覽無(wú)余的展現(xiàn)出來(lái)最岗,讀的讓人喘不過(guò)氣來(lái),甚至不忍繼續(xù)看下...
    清歡隨喜閱讀 1,146評(píng)論 2 2
  • 近些了朝捆,近些了般渡。都說(shuō)近鄉(xiāng)情怯。撲簌簌兩行熱淚芙盘。手抖抖一抔黃土驯用。遠(yuǎn)游的孩子啊,回來(lái)了儒老!從小便跟隨著父親背井離...
    你的樣子1314閱讀 794評(píng)論 1 51