利用android-gif-drawable开源库显示GIF动态图片

一、前言

android-gif-drawable是一个在Android显示gif图片的开源库,加载大的gif图片时不会出现OOM问题。

1. Drawable.Callback接口

Drawable.Callback 一般用于Drawable动画的回调控制,所有的Drawable子类都应该支持这个类,否则动画将无法在View上正常工作(View是实现了这个接口与BackgroundDrawable进行交互的)。像这篇文章就是分析点击效果(selector)的原理-----Android Drawable 分析

值得注意的是,Drawable的setCallback保存的Callback是弱引用,因此传进去的Callback对象不能是局部变量,不然Callback对象很快就会被回收导致动画无法继续播放。

/*如果你想实现一个扩展子Drawable的动画drawable,那么你可以通过setCallBack(android.graphics.drawable.Drawable.Callback)来把你实现的该接口注册到动画drawable 
*中。可以实现对动画的调度和执行 
*/   
public static interface Callback {  
        /** 
         * 当drawable重画时触发,这个点上drawable将被置为不可用(起码drawable展示部分不可用) 
         * @param 要求重画的drawable 
         */  
        public void invalidateDrawable(Drawable who);  
  
        /** 
         * drawable可以通过该方法来安排动画的下一帧。可以仅仅简单的调用postAtTime(Runnable, Object, long)来实现该方法。参数分别与方法的参数对 
         *应 
         * @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);  
  
        /** 
         *可以用于取消先前通过scheduleDrawable(Drawable who, Runnable what, long when)调度的某一帧。可以通过调用removeCallbacks(Runnable,Object)来实现 
         * @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中可以显示动态图片的控件:GifImageView 、GifImageButton和GifTextView。当需要赋的图像值是gif格式的图片的时候,会显示动态图片,如果是普通的静态图片,例如是png,jpg的,这个时候,gifImageView等这些控件的效果和ImageView是一样的,也就是说gif-drawable比ImageView更强大,使用的时候跟一般的控件一样。

<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是该开源库中最重要的一个类,GifDrawable可以通过以下方式构建GifDrawable对象

       //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实现了Animatable和MediaPlayerControl接口,因此可以通过很多方法对动画进行控制:

 - 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动态图片的一些相关信息

 - 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)

二、动态表情制作

要想高效地展示动态表情,需要考虑以下两方面:

  • 表情的匹配和表情的复用
  • 一个TextView里面可能会有很多表情,应该由具有什么特征的表情对该TextView进行刷新操作

表情的匹配和表情的复用

从服务器获取到的表情一般表示 [大笑] ,因此表情的匹配可以利用正则表达式进行匹配,从而找到正确的表情对象GifDrawable,然后作为Spanable中的ImageSpan在TextView进行播放。

但是每一次表情匹配都不可能新生成一个GifDrawable对象,这样会很浪费内存,甚至会出现OOM的问题,因此我们需要对表情进行复用,一旦匹配到的表情在之前已经匹配到了,则可以直接拿来复用。

下面写一个简单的类对表情进行管理,利用ConcurrentHashMap保存已经生成的表情,每次获取表情都ConcurrentHashMap获取,如果没有就再生成放到ConcurrentHashMap,可以看到保存GifDrawable是一个弱引用,这是保证一旦表情没有任何引用,系统可以尽快回收表情占用的内存。

public class EmotionManager {

    private static final int[] EMOTIONS_IDS = {};    //存储表情的Drawable ID

    private static final String[] EMOTIONS_EXPRESSES = {"[大笑]"};  //表情对应的含义

    private static ConcurrentHashMap<String, WeakReference<GifDrawable>> emotionCacheMap = null; //表情复用

    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并放进emotionCacheMap
        }
    }
}

表情的刷新

一个表情播放到下一帧的时候需要通知拥有它的TextView进行刷新,根据上面对Drawable.Callback接口的分析,我们可以调用每一个GifDrawable的setCallback函数对该GifDrawable存在的每一个TextView进行刷新。

但是该方案存在一个弊端,那就是如果一个TextView里面有很多表情则会导致该TextView刷新过快,最好的方法就是让TextView中刷新频率最高的表情去通知TextView进行刷新,在GifDrawable没有直接获取频率的接口,但是通过getDuration()和getFrameByteCount()可以计算频率。

从上面的分析可知我们需要重写GifDrawable,里面保存了一个该GifDrawable需要刷新的TextView的ConcurrentHashMap以及一个对该TextView列表进行刷新的Callback接口。

public class EmotionGifDrawable extends GifDrawable {
    
    //setCallback保存的是弱引用,因此这里需要保存GifDrawableCallBack的强引用,使其生命周期跟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);    //设置回调函数
    }
    
    //添加到TextView刷新列表
    private void addTextView(TextView textview) {

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

    }
    
    //从TextView刷新列表中移除, ListView复用需要进行移除
    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();    //回调进行TextView强制刷新操作
                }
            }
        }

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

        }

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

        }
    }
    
}

对于TextView中频率最高的表情的计算可以在表情匹配的时候进行,下面只贴出关键代码

    int minFrameinterval = Integer.MAX_VALUE;
    while(matcher.find()) {   //匹配到表情
        GifDrawable gifDrawable = EmotionManager.getEmotion(resources, matcher.group());   //获取表情
        if(gifDrawable != null) {
            int tempFrameInteval = gifDrawable.getDuration() / gifDrawable.getFrameByteCount();   //计算帧间隔
            if(tempFrameInteval < minFrameinterval) {
                maxFrequencyDrawable = gifDrawable;   //找到频率最高即帧间隔最短的Drawable
                minFrameinterval = tempFrameInteval;
            }
        }
    }

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,884评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,212评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,351评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,412评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,438评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,127评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,714评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,636评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,173评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,264评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,402评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,073评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,763评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,253评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,382评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,749评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,403评论 2 358

推荐阅读更多精彩内容