利用android-gif-drawable開(kāi)源庫(kù)顯示GIF動(dòng)態(tài)圖片

一、前言

android-gif-drawable是一個(gè)在Android顯示gif圖片的開(kāi)源庫(kù),加載大的gif圖片時(shí)不會(huì)出現(xiàn)OOM問(wèn)題。

1. Drawable.Callback接口

Drawable.Callback 一般用于Drawable動(dòng)畫(huà)的回調(diào)控制,所有的Drawable子類都應(yīng)該支持這個(gè)類,否則動(dòng)畫(huà)將無(wú)法在View上正常工作(View是實(shí)現(xiàn)了這個(gè)接口與BackgroundDrawable進(jìn)行交互的)。像這篇文章就是分析點(diǎn)擊效果(selector)的原理-----Android Drawable 分析。

值得注意的是,Drawable的setCallback保存的Callback是弱引用,因此傳進(jìn)去的Callback對(duì)象不能是局部變量,不然Callback對(duì)象很快就會(huì)被回收導(dǎo)致動(dòng)畫(huà)無(wú)法繼續(xù)播放。

/*如果你想實(shí)現(xiàn)一個(gè)擴(kuò)展子Drawable的動(dòng)畫(huà)drawable,那么你可以通過(guò)setCallBack(android.graphics.drawable.Drawable.Callback)來(lái)把你實(shí)現(xiàn)的該接口注冊(cè)到動(dòng)畫(huà)drawable 
*中??梢詫?shí)現(xiàn)對(duì)動(dòng)畫(huà)的調(diào)度和執(zhí)行 
*/   
public static interface Callback {  
        /** 
         * 當(dāng)drawable重畫(huà)時(shí)觸發(fā),這個(gè)點(diǎn)上drawable將被置為不可用(起碼drawable展示部分不可用) 
         * @param 要求重畫(huà)的drawable 
         */  
        public void invalidateDrawable(Drawable who);  
  
        /** 
         * drawable可以通過(guò)該方法來(lái)安排動(dòng)畫(huà)的下一幀??梢詢H僅簡(jiǎn)單的調(diào)用postAtTime(Runnable, Object, long)來(lái)實(shí)現(xiàn)該方法。參數(shù)分別與方法的參數(shù)對(duì) 
         *應(yīng) 
         * @param who The drawable being scheduled. 
         * @param what The action to execute. 
         * @param when The time (in milliseconds) to run 
         */  
        public void scheduleDrawable(Drawable who, Runnable what, long when);  
  
        /** 
         *可以用于取消先前通過(guò)scheduleDrawable(Drawable who, Runnable what, long when)調(diào)度的某一幀??梢酝ㄟ^(guò)調(diào)用removeCallbacks(Runnable,Object)來(lái)實(shí)現(xiàn) 
         * @param who The drawable being unscheduled. 
         * @param what The action being unscheduled. 
         */  
        public void unscheduleDrawable(Drawable who, Runnable what);  
    }  

2. gif-drawable控件

gif-Drawable一共提供了3中可以顯示動(dòng)態(tài)圖片的控件:GifImageView 、GifImageButton和GifTextView。當(dāng)需要賦的圖像值是gif格式的圖片的時(shí)候,會(huì)顯示動(dòng)態(tài)圖片,如果是普通的靜態(tài)圖片,例如是png,jpg的,這個(gè)時(shí)候,gifImageView等這些控件的效果和ImageView是一樣的,也就是說(shuō)gif-drawable比ImageView更強(qiáng)大,使用的時(shí)候跟一般的控件一樣。

<pl.droidsonroids.gif.GifImageView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/src_anim"
    android:background="@drawable/bg_anim"
    />

3. GifDrawable類

GifDrawable是該開(kāi)源庫(kù)中最重要的一個(gè)類,GifDrawable可以通過(guò)以下方式構(gòu)建GifDrawable對(duì)象

       //asset file
        GifDrawable gifFromAssets = new GifDrawable( getAssets(), "anim.gif" );

        //resource (drawable or raw)
        GifDrawable gifFromResource = new GifDrawable( getResources(), R.drawable.anim );

        //byte array
        byte[] rawGifBytes = ...
        GifDrawable gifFromBytes = new GifDrawable( rawGifBytes );

        //FileDescriptor
        FileDescriptor fd = new RandomAccessFile( "/path/anim.gif", "r" ).getFD();
        GifDrawable gifFromFd = new GifDrawable( fd );

        //file path
        GifDrawable gifFromPath = new GifDrawable( "/path/anim.gif" );

        //file
        File gifFile = new File(getFilesDir(),"anim.gif");
        GifDrawable gifFromFile = new GifDrawable(gifFile);

        //AssetFileDescriptor
        AssetFileDescriptor afd = getAssets().openFd( "anim.gif" );
        GifDrawable gifFromAfd = new GifDrawable( afd );

        //InputStream (it must support marking)
        InputStream sourceIs = ...
        BufferedInputStream bis = new BufferedInputStream( sourceIs, GIF_LENGTH );
        GifDrawable gifFromStream = new GifDrawable( bis );

        //direct ByteBuffer
        ByteBuffer rawGifBytes = ...
        GifDrawable gifFromBytes = new GifDrawable( rawGifBytes );

GifDrawable實(shí)現(xiàn)了Animatable和MediaPlayerControl接口,因此可以通過(guò)很多方法對(duì)動(dòng)畫(huà)進(jìn)行控制:

 - stop() - stops the animation, can be called from any thread
 - start() - starts the animation, can be called from any thread
 - isRunning() - returns whether animation is currently running or not
 - reset() - rewinds the animation, does not restart stopped one
 - setSpeed(float factor) - sets new animation speed factor, eg. passing 2.0f will double the animation speed
 - seekTo(int position) - seeks animation (within current loop) to given position (in milliseconds) Only seeking forward is  - supported
 - getDuration() - returns duration of one loop of the animation
 - getCurrentPosition() - returns elapsed time from the beginning of a current loop of animation

在GifDrawable里面也可以獲取Gif動(dòng)態(tài)圖片的一些相關(guān)信息

 - getLoopCount() - returns a loop count as defined in NETSCAPE 2.0 extension
 - getNumberOfFrames() - returns number of frames (at least 1)
 - getComment() - returns comment text (null if GIF has no comment)
 - getFrameByteCount() - returns minimum number of bytes that can be used to store pixels of the single frame
 - getAllocationByteCount() - returns size (in bytes) of the allocated memory used to store pixels of given GifDrawable
 - getInputSourceByteCount() - returns length (in bytes) of the backing input data
 - toString() - returns human readable information about image size and number of frames (intended for debugging purpose)

二、動(dòng)態(tài)表情制作

要想高效地展示動(dòng)態(tài)表情,需要考慮以下兩方面:

  • 表情的匹配和表情的復(fù)用
  • 一個(gè)TextView里面可能會(huì)有很多表情,應(yīng)該由具有什么特征的表情對(duì)該TextView進(jìn)行刷新操作

表情的匹配和表情的復(fù)用

從服務(wù)器獲取到的表情一般表示 [大笑] ,因此表情的匹配可以利用正則表達(dá)式進(jìn)行匹配,從而找到正確的表情對(duì)象GifDrawable,然后作為Spanable中的ImageSpan在TextView進(jìn)行播放。

但是每一次表情匹配都不可能新生成一個(gè)GifDrawable對(duì)象,這樣會(huì)很浪費(fèi)內(nèi)存,甚至?xí)霈F(xiàn)OOM的問(wèn)題,因此我們需要對(duì)表情進(jìn)行復(fù)用,一旦匹配到的表情在之前已經(jīng)匹配到了,則可以直接拿來(lái)復(fù)用。

下面寫(xiě)一個(gè)簡(jiǎn)單的類對(duì)表情進(jìn)行管理,利用ConcurrentHashMap保存已經(jīng)生成的表情,每次獲取表情都ConcurrentHashMap獲取,如果沒(méi)有就再生成放到ConcurrentHashMap,可以看到保存GifDrawable是一個(gè)弱引用,這是保證一旦表情沒(méi)有任何引用,系統(tǒng)可以盡快回收表情占用的內(nèi)存。

public class EmotionManager {

    private static final int[] EMOTIONS_IDS = {};    //存儲(chǔ)表情的Drawable ID

    private static final String[] EMOTIONS_EXPRESSES = {"[大笑]"};  //表情對(duì)應(yīng)的含義

    private static ConcurrentHashMap<String, WeakReference<GifDrawable>> emotionCacheMap = null; //表情復(fù)用

    private static GifDrawable getEmotion(Resources resources, String expression) {
        if(emotionCacheMap == null) {
            emotionCacheMap = new ConcurrentHashMap<>();
        }

        WeakReference<GifDrawable> reference = emotionCacheMap.get(expression);
        if(reference != null && reference.get() != null) {
            return reference.get();
        } else {
            //新生成GifDrawable并放進(jìn)emotionCacheMap
        }
    }
}

表情的刷新

一個(gè)表情播放到下一幀的時(shí)候需要通知擁有它的TextView進(jìn)行刷新,根據(jù)上面對(duì)Drawable.Callback接口的分析,我們可以調(diào)用每一個(gè)GifDrawable的setCallback函數(shù)對(duì)該GifDrawable存在的每一個(gè)TextView進(jìn)行刷新。

但是該方案存在一個(gè)弊端,那就是如果一個(gè)TextView里面有很多表情則會(huì)導(dǎo)致該TextView刷新過(guò)快,最好的方法就是讓TextView中刷新頻率最高的表情去通知TextView進(jìn)行刷新,在GifDrawable沒(méi)有直接獲取頻率的接口,但是通過(guò)getDuration()和getFrameByteCount()可以計(jì)算頻率。

從上面的分析可知我們需要重寫(xiě)GifDrawable,里面保存了一個(gè)該GifDrawable需要刷新的TextView的ConcurrentHashMap以及一個(gè)對(duì)該TextView列表進(jìn)行刷新的Callback接口。

public class EmotionGifDrawable extends GifDrawable {
    
    //setCallback保存的是弱引用,因此這里需要保存GifDrawableCallBack的強(qiáng)引用,使其生命周期跟GifDrawable一樣
    private GifDrawableCallBack callBack;  
    
    private ConcurrentHashMap<Integer, WeakReference<TextView>> textViewMap;   //GifDrawbale需要刷新的TextView列表
    
    public EmotionGifDrawable(@NonNull Resources resources, @DrawableRes @RawRes int id) throws Resources.NotFoundException, IOException{
        super(resources, id);
        textViewMap = new ConcurrentHashMap<>();
        callBack = new GifDrawableCallBack();
        setCallback(callBack);    //設(shè)置回調(diào)函數(shù)
    }
    
    //添加到TextView刷新列表
    private void addTextView(TextView textview) {

        //利用TextView的hashCode()保存到textViewMap

    }
    
    //從TextView刷新列表中移除, ListView復(fù)用需要進(jìn)行移除
    private void removeTextView(TextView textView) {
        
    }
    
    
    class GifDrawableCallBack implements Callback {

        @Override
        public void invalidateDrawable(Drawable who) {
            for(Map.Entry<Integer, WeakReference<TextView>> entry : textViewMap.entrySet()) {
                if(entry.getValue().get() != null) {
                    entry.getValue().get().invalidate();    //回調(diào)進(jìn)行TextView強(qiáng)制刷新操作
                }
            }
        }

        @Override
        public void scheduleDrawable(Drawable who, Runnable what, long when) {

        }

        @Override
        public void unscheduleDrawable(Drawable who, Runnable what) {

        }
    }
    
}

對(duì)于TextView中頻率最高的表情的計(jì)算可以在表情匹配的時(shí)候進(jìn)行,下面只貼出關(guān)鍵代碼

    int minFrameinterval = Integer.MAX_VALUE;
    while(matcher.find()) {   //匹配到表情
        GifDrawable gifDrawable = EmotionManager.getEmotion(resources, matcher.group());   //獲取表情
        if(gifDrawable != null) {
            int tempFrameInteval = gifDrawable.getDuration() / gifDrawable.getFrameByteCount();   //計(jì)算幀間隔
            if(tempFrameInteval < minFrameinterval) {
                maxFrequencyDrawable = gifDrawable;   //找到頻率最高即幀間隔最短的Drawable
                minFrameinterval = tempFrameInteval;
            }
        }
    }

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容