【Android实战】记录自学自己定义GifView过程,能同一时候支持gif和其它图片!【有用篇】...
之前寫了一篇博客。《【Android實戰】記錄自學自己定義GifView過程,具體解釋屬性那些事!
【學習篇】》
關于自己定義GifView的,具體解說了學習過程及遇到的一些類的解釋,然后完畢了一個項目,能通過在xml增加自己定義 view (MyGifView)中增加自己定義屬性(my:gif_src = “@drawable/coffee”)。達到播放gif圖片的效果。
可是。有幾個問題
1.gif_src 屬性僅僅支持 gif 圖,并不支持其它類型的圖片
2.僅僅支持默認的引用圖片,不能另外設置
問題一
gif_src 屬性僅僅支持 gif 圖,并不支持其它類型的圖片。
解決思路:
ImageView本身有個屬性 src 是定義好的。已經能夠用它播放靜態圖片。假設再能通過它播放動態圖片,不就解決這個問題啦?!
于是查看 ImageView 類的源代碼,看到構造函數
public ImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {//...final TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ImageView, defStyleAttr, defStyleRes);Drawable d = a.getDrawable(com.android.internal.R.styleable.ImageView_src);if (d != null) {setImageDrawable(d);}//... }有沒有非常眼熟?!
對,之前自己定義屬性的時候用過!這里只是把屬性路徑改了。之前我們用的是自己定義的路徑 R.styleable.GifView 。
于是乎。我也想著,要是能在繼承類(MyGifView)里面復制上段代碼,然后再用movie轉化,轉換成功說明是 gif。就用之前的方法播放。轉換失敗說明是其它格式的圖片,就交給 ImageView 自己處理!
真是好辦法!
然而。根本不能這么用:
不能導入internal的包!
可是思路是對的!
通過參考《 Android PowerImageView實現。能夠播放動畫的強大ImageView》
得知了能夠用反射!
中心代碼:
* * @param a 屬性組 * @param context * @return 返回布局文件里指定圖片資源所相應的id,沒有指定不論什么圖片資源就返回0。 */
private int getResourceId(TypedArray a, Context context) { try { Field field = TypedArray.class.getDeclaredField("mValue"); field.setAccessible(true); TypedValue typedValueObject = (TypedValue) field.get(a); return typedValueObject.resourceId; } catch (Exception e) { e.printStackTrace(); } finally { if (a != null) { a.recycle(); } } return 0; }之前在自己定義view初始化中的代碼。我是用得到自己定義屬性值的方法獲取gif的數據
int resId = typedArray.getResourceId(R.styleable.GifView_gif_src, 0); //gif_src屬性相應值如今僅僅須要改這一句就好啦!
//int resId = typedArray.getResourceId(R.styleable.GifView_gif_src, 0); //gif_src屬性相應值int resId = getResourceId(typedArray, context); //src屬性相應值然后后面都不用改啦!
(可是轉換成 InputStream 的時候。還是要加一句推斷 if (resId != 0)再進行轉換)
if (resId != 0) {InputStream iStream = getResources().openRawResource(resId); //此方法能通過資源文件id查找到資源文件并轉化為輸入流mMovie = Movie.decodeStream(iStream); //輸入流轉化為Movie (mMovie 為全局變量,類型 Movie) }好了問題解決。如今能夠在xml用src屬性指向.gif文件,并且進行正常播放了!
問題二
僅僅支持默認的引用圖片,不能另外設置
解決思路:
從外面設置無非就是外面調用setImageResource(int resId),setImageDrawable(Drawable drawable),setImageBitmap(Bitmap bm)等這些方法去改變 ImageView 屬性 src 所相應的值!
那么。重寫這些方法。把資源改成我們的 movie 就好啦。so easy!
首先重寫setImageResource(int resId)
@Override public void setImageResource(int resId) {if (resId != 0) {InputStream iStream = getResources().openRawResource(resId);setMovie(iStream);if (mMovie == null) {super.setImageResource(resId);}} else {super.setImageResource(resId);}invalidate(); } /*** 設置movie* @param iStream 輸入流*/ public void setMovie(InputStream iStream) {mMovie = Movie.decodeStream(iStream);if (mMovie == null) { //說明不是gif。直接退出return;}//設置圖片寬高Bitmap bitmap = BitmapFactory.decodeStream(iStream);if (bitmap == null) {return;}mWidth = bitmap.getWidth();mHeight = bitmap.getHeight();bitmap.recycle(); }然后在外面(比方MainActivity),調用gifView.setImageResource(R.drawable.coffee),能夠顯示gif,其它格式的圖片也能夠正常顯示。
but…
出現了一個bug…
就是如今必須在xml里面的自己定義MyGifView增加默認的 src 引用 或者 backgroud 附初始值,不然會報錯崩潰,假設不想增加默認圖片。能夠把background設置為透明 #00000000
報錯的原因,大概是沒設置src屬性時。調用反射int resId = getResourceId(typedArray, context);得到的 resId 也并不為0 (具體得到的是什么我也還不知)。然后進入 if 語句執行InputStream iStream = getResources().openRawResource(resId);轉換流的時候報了空指針,導致程序崩潰。
依照設置默認src或者backgroud的方法能夠臨時解決。假設廣大網友知道是什么原因,有什么更好的辦法解決它,懇求告知一下!
接著重寫setImageDrawable(Drawable drawable)和setImageBitmap(Bitmap bm)
=-=-=-=-=-=-=-=-= 為了完畢以下的實現,另花費了非常久時間,思緒可能和前面不大連貫了 =-=-=-=-=-=-=-=-=
我本來以為會如setImageResource(int resId)一樣順利,但事實上按原來的思路并不能夠轉換成gif,而是轉換成了png/jepg!最后還是借助了三方包才勉強完畢任務。
接下來我按順序解說下我實現的過程。
首先講下原來的想法以及為什么后來推翻了。
按著setImageResource(int resId)的實現思路。setImageDrawable(Drawable drawable)應該也就是把 drawable 先轉換成 input sream,然后再轉換成movie,假設成功就說明是gif,不成功說明是其它格式則調用父類方法。
@Override public void setImageDrawable(Drawable drawable) {if(drawable == null) {super.setImageDraable(drawable);} else {mWidth = drawable.getIntrinsicWidth(); //獲得寬mHeight = drawable.getIntrinsicHeight(); //獲得高InputStream iStream = FomatUtils.Drawable2InputStream(drawable); //通過工具類將drawable轉換成input streamsetMovie(iStream);if (mMovie == null) { //說明不是gifsuper.setImageDrawable(drawable);}}invalidate(); }@Override public void setImageBitmap(Bitmap bm) {this.setImageDrawable(new BitmapDrawable(getContext().getResources(), bm)); }這個時候我們在Activity增加語句調用setImageDrawable()方法。在執行,出現的是靜態圖片!
說明代碼是有問題的,焦點在drawable轉換成inputstream的地方
InputStream iStream = FomatUtils.Drawable2InputStream(drawable); //通過工具類將drawable轉換成input stream看下轉換類的具體代碼 (參考《Android Bitmap與DrawAble與byte[]與InputStream之間的轉換工具類》)
/** * Bitmap與DrawAble與byte[]與InputStream之間的轉換工具類 * @author azz */ public class FormatUtils { /*** drawable -> input stream*/public InputStream Drawable2InputStream(Drawable d) { //drawable -> bitmapBitmap bitmap = this.Drawable2Bitmap(d);//bitmap -> input stream return this.Bitmap2InputStream(bitmap); } /*** drawable -> bitmap*/public Bitmap Drawable2Bitmap(Drawable drawable) { Bitmap bitmap = Bitmap .createBitmap( drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); drawable.draw(canvas); return bitmap; } /*** bitmap -> input stream*/public InputStream Bitmap2InputStream(Bitmap bm, int quality) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); bm.compress(Bitmap.CompressFormat.PNG, quality, baos); //就是這里限制了轉換成PNG類型InputStream is = new ByteArrayInputStream(baos.toByteArray()); return is; } }注意第三個轉換函數Bitmap2InputStream()方法內的第二句
bm.compress(Bitmap.CompressFormat.PNG, quality, baos);非常明顯這里將Bitmap壓縮為了PNG格式。也就是說GIF被壓縮成了PNG!
這個時候非常容易想到,把PNG改成GIF不就能夠了嘛!~
可是點擊進入 CompressFormat 類后發現,僅僅支持三種格式
public enum CompressFormat {JPEG (0),PNG (1),WEBP (2); //Sdk-14后開始支持 }而這三種都不能實現壓縮成gif格式的流。
這個時候我就突發奇想了,能不能不轉換成 input stream!看看Movie還支持其它的什么轉換方法么!
結果是這種:
Movie.decodeByteArray(byte[] data, int offset, int length)
Movie.decodeFile(String pathName)
Movie.decodeStream(InputStream is)
1.decodeByteArray通過 byte[] 轉換的話。不管bitmap還是drawable都要經過下一步驟
/*** bitmap -> byte[]*/public byte[] Bitmap2Bytes(Bitmap bm) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); bm.compress(Bitmap.CompressFormat.PNG, 100, baos); return baos.toByteArray(); }能夠看到還是要壓縮。所以這種方法放棄。
2.decodeFile通過文件轉化的方法好像行之有效。并且Image自帶方法setImageURL(Uri uri),那么重寫它看看!
@Override public void setImageURI(Uri uri) {mMovie = Movie.decodeFile(uri.toString());if (mMovie == null) {super.setImageURI(uri);} }懷著激動的心情執行。
結果 —— 并沒有顯示不論什么東西。
—(2015.7.21更新。找到方法破解該問題!
可略過以下一大段直接跳到最后看更新內容!)—
希望破滅了~
問度娘要安慰~
檢索后。發現大部分情況都是將GIF轉換成了PNG/JEPG就不了了之了,去 stackoverflow 發現有人問卻也沒有人給出行之有效的辦法,去Github找各種第三方。發現非常多也僅僅是和我【學習篇】實現一樣,并不能解決實際問題(我如今項目確實要用到,從sd卡讀取圖片并顯示,支持gif和其它格式圖片)。
搜索了一下午。也有了一些思路,大概例如以下:
方案1.通過先把 gif 圖片轉換為若干幀的 bitmap 保存起來,然后等要使用的時候,再合成 gif 。并利用線程播放。
(參看:《Android 載入.gif格式圖片》。
這種方法聽上去就比較麻煩,看了代碼量又好大我沒時 (zi) 間 (xi) 看……)
方案2.自己寫一個壓縮類,實現壓縮 gif 格式圖片。
(參看:《java圖片壓縮處理 支持gif》和《最終搞定多張JPG圖片轉成GIF動畫這個難題,解決方法例如以下。》。
這種方法聽上去簡單好用,可是代碼量好大我沒仔 (shi) 細 (jian) 看……)
方案3.通過第三方寫好的拿來用。依據需求更改源代碼。
(參看:《android開源庫android-gif-drawable的使用》和“GifView項目源代碼”。這兩個好像看評論第一個更好,用 JNI 攻克了內存泄露的問題。
可是。我用的是第二個……第二個自帶 javadoc。自學沒有問題。)
—(2015.8.28更新,還是把我逼到用jni的地步,已經會用。不難。比movie效率高!
往后跳過看。)—
懶人看過來:
我是個聰明人。也就是俗稱的懶人~我一定是朝著“怎么能高速解決這個問題”這個方向走的。我如今攻克了,但并非完美地解決的方法,鼓舞大家都是勤快人。能自己去琢磨~也能夠參考我的做法。
首先下載“GifView項目源代碼”。文件夾結構是這個樣子的
能夠看到有個Activity!~那么說明能夠直接執行,我們看一下。
哇,感覺好強大的樣子。
可是翻閱Activity的實現發現,它都是調用項目res資源的gif,這個我們已經實現了,看看它還有沒有其它的設置圖片方法?
然后我們看看doc,哇好全的樣子(自學就靠它了!)
在GifView的API中。我們發現了三個方法:
經試驗。setGifImage(String filePath)能夠播放本地(指SD卡)gif,可是也僅僅是支持 .gif 格式的,假設路徑目標是其它格式(比方.png),程序就會掛掉。
感覺又回到了原點……忙了一天了。毫無成果。非常挫敗。
這時候我懶人思想冒了出來:既然是讀取文件路徑,那就說明能夠預先推斷后綴名是否是gif。假設是的話就調用該方法,不是調用默認方法不就能夠了。
另外,我發現。GifView并沒有重寫父類的“onDraw()”,”onMeasure()”。“setImageResource(int)”等方法,而我的 MyGifView 剛好寫了,于是結合一下。用 MyGifView 繼承 GifView!在MyGifView進行改動!
@Override public void setImageURI(Uri uri) {String path = uri.toString();if (isGif(path)) { //依據路徑名推斷后綴是否為gifsetGifImage(path); //調用父類GifView的方法} else {this.pauseGifAnimation(); //暫停之前動畫,不然設置別的圖片的時候,原來的gif還在播放動畫super.setImageURI(uri); //調用原始父類ImageView的方法} } //以下兩個方法能夠寫到工具類里 /*** @Description 推斷是否是gif圖片* @param path 文件路徑* @return true 是gif; false 不是gif*/ public boolean isGif(final String path) {if ("gif".equals(getExtFromFileName)) {return true;}return false; } /*** @Description 獲取文件后綴名* @param fileName 文件名稱或文件路徑* @return 后綴名*/ public String getExtFromFileName(final String fileName) {int dot = fileName.lastIndexOf('.'); //取得最后一個.的位置if (dot != -1) {return fileName.substring(dot + 1, fileName.length());}return ""; }在Activity里面調用
myGifView.setImageURI(Uri.parse("mnt/sda/sda1/test/coffee.gif"));//myGifView.setImageURI(Uri.parse("mnt/sda/sda1/test/me.png"));這個時候執行,發現gif和普通圖片都能正常顯示。可是卡頓非常嚴重!
用GifView的設置方法卻不會。
原因我也找到了,是由于我的MyGifView重寫了setImageDrawable(Drawable drawable)和setImageBitmap(Bitmap bitmap),依據打印發現播放的時候,這兩個方法頻繁被調用,可能GifView播放動畫的時候用到了這兩個方法,反正我如今也用不到這兩個方法,于是。我就干脆屏蔽掉了。
屏蔽掉之后果然不卡頓了。
最后有人要問了,那我的圖片不在本地怎么辦呀?我從網上下下來的圖片怎么辦呀?
我的懶人思想:
方案1. 你下下來先保存嘛……
方案2. 假設你能夠得到byte[]數據的話,能夠試試Movie的處理byte[]的辦法,也能夠自己把byte[]轉換成InputStream。然后調用setMovie(InputStream),還能夠試試用GifView處理byte[]的方法。
總而言之,能繞過bitmap轉input stream就好辦!
原來這篇博客寫了八千字了……該結貼了。
源代碼地址:https://github.com/Xieyupeng520/MyGifView_V1.1
2015.7.21 更新。解決 Movie.decodeFile 不起作用的問題
本來用上面的第三方攻克了也挺好。可是我發現幾個問題,第三方的代碼播放幀數較多的 gif 圖片卡頓非常嚴重!并且 gif 放大后清晰度也失真嚴重。
非常幸運的是。我在網上找解決的方法的時候,找到了個非常easy的方法,還是用Movie。并且用 Movie 通過流的方式播放 gif 的效果是最好的。和原圖無差。
這種方法我之前也用過。就是用 Movie 的 decodeFile 方法,以下是 GifView 里面重寫父類的 setImageURI 方法。
@Override public void setImageURI(Uri uri) {mMovie = Movie.decodeFile(uri.toString());if (mMovie == null) {super.setImageURI(uri);} }這個之前試過是不行的。
看我找到的解決的方法。
方法一:
參考了“貼吧五樓”和博客 《android 播放網絡或本地gif格式的動態圖片》后。得到的解決的方法是:
將FileInputStream轉化為btye[]數組。然后調用Movie.decodeByteArray(byte[] array,0,array.length); 去完畢。
寫成代碼是這個樣子:
@Override public void setImageURI(Uri uri) {InputStream is = new FileInputStream(uri.toString());//把 sream 轉換成 byte[]byte[] array = streamToBytes(iStream);mMovie = Movie.decodeByteArray(array, 0, array.length);if (mMovie == null) {super.setImageURI(uri);}//設置圖片寬高Bitmap bitmap = BitmapFactory.decodeByteArray(array, 0, array.length);if (bitmap != null) {mWidth = bitmap.getWidth();mHeight = bitmap.getHeight();bitmap.recycle(); //不須要了,釋放掉} }//把 sream 轉換成 byte[] private byte[] streamToBytes(InputStream is) {ByteArrayOutputStream os = new ByteArrayOutputStream(1024); //親測也可不寫1024byte[] buffer = new byte[1024]; //緩存bufferint len;try {while ((len = is.read(buffer)) >= 0) {os.write(buffer, 0, len); //寫入輸出流}} catch (Exception e) {e.printStackTrace();}return os.toByteArray(); }方法二:
假設你連方法一都嫌麻煩的話,那方法二真的是代碼少到死!
直接上代碼!
只是有個問題是,Bitmap bitmap = BitmapFactory.decodeStream(iStream);這一句得到的bitmap為null,用方法一不會出現此問題。
參考:《解決Android中Movie導入播放GIF圖片文件異常IOException.reset》
我也真是運氣,點進去一個“不相關”的問題,都能找到解決的方法。
所以提醒大家找問題的時候,不要局限于你自己的那幾個關鍵字哦!
~
2015.8.28 更新!使用強大的android-gif-drawable開源庫,效率比Movie還高!
命運多舛,又出現新問題了,7.21更新的方法在4.0。4.2,5.0的機器上測試都沒問題,卻發如今Android 4.4出現調用movie.draw(canvas,0,0)崩潰的情況。我也崩潰了。
于是后來我用了第三方調用jni的android-gif-drawable開源庫。之前不用它就是怕麻煩,用過之后發現不像想象中那樣復雜。如今記錄一下使用過程。
首先打開《android開源庫android-gif-drawable的使用》,過一下前面8點,講了怎么樣把 jni 拷到自己項目中。
后面的就是一些 API 了。
簡單使用,先新建一個GifDrawable,然后把GifDrawable設置到 GifImageView / GifImageButton / GifTextView 中。就OK了!
GifImageView gif = (GifImageView) findViewById(R.id.hisGifView);GifDrawable gifFromResource = new GifDrawable( getResources(), R.drawable.run );gif.setImageDrawable(gifFromResource);值得一提的是,文章其中用的是1.0.8版本號,如今盡管已經更新到了1.1.9版本號了。可是我用的仍然是1.0.8的,由于1.1.9的so直接導入執行報錯,而我還不知道怎樣解決。
1.0.8版本號缺點是不能在Android 5.0 + 的手機上正常執行。
8.28更新-Demo源代碼下載:https://github.com/Xieyupeng520/MyGifView_V1.3
假設你有不論什么問題。歡迎留言告訴我!
總結
以上是生活随笔為你收集整理的【Android实战】记录自学自己定义GifView过程,能同一时候支持gif和其它图片!【有用篇】...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 7.Redis常用命令:ZSet
- 下一篇: C++ multimap 的插入,遍历,