一、前言
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;
}
}
}