超大圖片的顯示:BitmapRegionDecoder

不少安卓開發(fā)者都有圖片加載的處理經(jīng)驗(yàn),比如通過壓縮節(jié)省圖片加載中對(duì)內(nèi)存的消耗尊惰。
我們經(jīng)常做的是把一張1280之類大小的圖片以適應(yīng)屏幕大小的尺寸展現(xiàn)出來,同時(shí)能夠通過縮放來觀察。
不過這是一般水平,通過壓縮來處理的話通常會(huì)導(dǎo)致在最大尺寸放大后看不清細(xì)節(jié)峡扩,比如拿到一張蒼老師...哦不,拿到一張清明上河圖障本,或者一張世界地圖教届,這個(gè)時(shí)候我們要保證在最大限度的放大后仍然能夠看清楚每個(gè)人物每個(gè)城市,一般的壓縮的方案就不合適了驾霜。

這里我們要討論的是如何用局部解析(BitmapRegionDecoder)來做到在不占用過多內(nèi)存的情況下實(shí)現(xiàn)超大圖的縮放案训。

慣例貼源碼:XPhotoView Demo

XPhotoView繼承ImageView實(shí)現(xiàn)了超大圖加載,Demo中演示了如何在Pager加載靜態(tài)圖片和動(dòng)圖寄悯,同時(shí)也支持各種手勢(shì)操作萤衰。
我在公司的產(chǎn)品上自定義了XPhotoView,在包括聊天列表猜旬,動(dòng)圖播放脆栋,還有高清大圖查看的功能上已經(jīng)驗(yàn)證了它的穩(wěn)定和高效,平時(shí)的開發(fā)中可以直接使用洒擦。

超大圖片加載和局部解析

對(duì)于普通的圖片椿争,我們加載的思路很簡單就是壓縮大小,用Options來獲得大小然后和當(dāng)前屏幕大小進(jìn)行比較熟嫩,然后以一定的值壓縮秦踪。但是這樣帶來的問題是,壓縮后的圖片會(huì)丟失細(xì)節(jié)掸茅,如果是小澤...呸椅邓,如果是清明上河圖壓縮到屏幕大小,放大后怕是人都看不見昧狮。而整張圖片加載肯定是行不通的景馁,內(nèi)存絕對(duì)立馬爆。
解決方案很簡單逗鸣,我們只加載圖片的局部區(qū)域合住,這部分區(qū)域適配屏幕大小绰精,配合手勢(shì)移動(dòng)的時(shí)候更新顯示對(duì)應(yīng)的區(qū)域就可以了。
Android提供了BitmapRegionDecoder來進(jìn)行圖片的局部解析透葛,這是XPhotoView的主要思路笨使。
剩下的就是如何高效的加載的問題了,如何設(shè)計(jì)代碼邏輯僚害,讓它能夠快速的響應(yīng)手勢(shì)動(dòng)作呢硫椰。

局部解析的代碼邏輯

代碼結(jié)構(gòu)

XPhotoView的代碼概要如下所示,

--|--|--XPhotoView
  |  |
  |  |--PhotoViewAttacher
  |--GestureManager

大體可以分為兩部分贡珊,XPhotoView和PhotoViewAttacher負(fù)責(zé)圖片的加載和解析最爬,GestureManager負(fù)責(zé)手勢(shì)的判斷和響應(yīng)。整個(gè)庫對(duì)外暴露的只是XPhotoView的幾個(gè)public方法用來setImage和相關(guān)的Listener门岔,還有是否是Gif的參數(shù)爱致。
Attacher本身只負(fù)責(zé)圖片的拆分解析和渲染過程,同時(shí)Bitmap也是保存在Attacher中寒随。Attacher和XPhotoView之間通過Interface互相調(diào)用糠悯,以此隔離。

Attacher 的解析過程

我們暫時(shí)忽略Gif的部分妻往,先描述一下Attacher的思路互艾。
Attacher有一個(gè)內(nèi)部子類BitmapUnit和BitmapGridStrategy,初始圖片會(huì)被BitmapRegionDecoder切割為 N*M 的網(wǎng)格讯泣,然后存儲(chǔ)在BitmapUnit[N][M]二維數(shù)組 mGrids 中纫普。

image.png

以清明上河圖為例,圖中高亮的線條把圖片分割為三部分好渠,就是說我們用 BitmapUnit[1][3] 來存儲(chǔ)這張圖片昨稼。這么做的原因是,當(dāng)我們放大圖片來查看的時(shí)候拳锚,只需要解析單個(gè)格子以及它相鄰格子里的圖片假栓。
當(dāng)然在我們以適配屏幕的條件下查看全圖時(shí),是經(jīng)過mSampleSize比例壓縮的霍掺,也就是說在mGrids中的Bitmap是壓縮過后的占小內(nèi)存的位圖匾荆,不用擔(dān)心OOM的問題。

    /** 當(dāng)前圖片的的采樣率 */
    private int mSampleSize = 0;
    /***
     * View Rect
     * View 坐標(biāo)系*/
    private Rect mViewRect = new Rect();

    /** 原圖 Rect
     *  Bitmap 坐標(biāo)系 */
    private Rect mImageRect = new Rect();

    /**
     * 實(shí)際展示的 Bitmap 大小
     * Bitmap 坐標(biāo)系 */
    private RectF mShowBitmapRect = new RectF();
    /**
     * view 相對(duì) Show Bitmap 的坐標(biāo)
     * Bitmap 坐標(biāo)系 */
    private Rect mViewBitmapRect = new Rect();

以上是Attacher中的關(guān)鍵變量杆烁,整個(gè)解析和渲染的過程基于這四個(gè)Rect的坐標(biāo)牙丽。

現(xiàn)在我們開始整個(gè)流程。

初始化
/**
 * @param bitmap 設(shè)置 bitmap
 * @param cache 是否cache
 */
void setBitmap(Bitmap bitmap, boolean cache);

/**
 * @param is 設(shè)置輸入流
 * @param config config
 */
void setInputStream(InputStream is, Bitmap.Config config);

這兩個(gè)是Attacher定義的對(duì)外接口兔魂,它只允許兩種方式來設(shè)置圖片剩岳,不管是哪個(gè)方式,都會(huì)轉(zhuǎn)換為InputStream對(duì)象mDecodeInputStream入热,來作為BitmapRegionDecoder的來源拍棕。
若以setBitmap()方法初始化的話,會(huì)多設(shè)置一個(gè)mSrcBitmap勺良,當(dāng)進(jìn)行局部解析時(shí)就不會(huì)通過BitmapRegionDecoder來解析绰播,而是會(huì)直接從mSrcBitmap中createBitmap對(duì)應(yīng)的區(qū)域出來。這種方式的前提是默認(rèn)不會(huì)出現(xiàn)OOM尚困,畢竟已經(jīng)可以整個(gè)Bitmap作為參數(shù)傳進(jìn)來了蠢箩,但是不能保證在后面createBitmap時(shí)不會(huì)OOM,所以不提倡用這個(gè)方法來初始化事甜。

在調(diào)用這兩個(gè)方法任何一個(gè)之后谬泌,都會(huì)調(diào)用initialize()來初始化需要的線程和Handler,

/** 初始化所需參數(shù)和線程*/
private synchronized void initialize(Bitmap.Config config)

然后我們來到setBitmapDecoder(final InputStream is)方法逻谦,此時(shí)我們開始真正的拆圖和解析掌实。這個(gè)方法是所有的起點(diǎn),而且只會(huì)也只應(yīng)該走一次邦马。
它會(huì)把mInstanceDecoderRunnable丟給handler然后開始運(yùn)行贱鼻,在解析完成后通過回調(diào)告知上層解析完畢,可以進(jìn)行關(guān)閉進(jìn)度條之類的操作滋将。

獲取圖片初始顯示參數(shù)

此時(shí)我們會(huì)調(diào)用這個(gè)方法

private void initiateViewRect(int viewWidth, int viewHeight)

但是第一次調(diào)用的時(shí)候是在onDraw之前邻悬,在setImage之后,此時(shí)我們并不知道具體的Canvas的大小随闽,因此沒法確定縮放比例父丰,還有其他的Rect所需要的初始化的具體值。因此此時(shí)mViewRect的值都還是0掘宪,作為參數(shù)傳進(jìn)來后經(jīng)過校驗(yàn)是無效值蛾扇,則會(huì)退出此次的方法。
這時(shí)我們來看看draw()方法添诉,

@Override
public boolean draw(@NonNull Canvas canvas, int width, int height) {
    if (isNotAvailable()) {
        return false;
    }

    if (mSrcBitmap != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        int mw = canvas.getMaximumBitmapWidth();
        int mh = canvas.getMaximumBitmapHeight();

        /**
         * 如果圖片太大屁桑,直接使用bitmap 會(huì)占用很大內(nèi)存,所以建議緩存為文件再顯示
         */
        if (mSrcBitmap.getHeight() > mh || mSrcBitmap.getWidth() > mw) {
            //TODO
        }
    }

    /**
     * 更新視圖或者畫出圖片
     */
    return !checkOrUpdateViewRect(width, height) && mBitmapGrid.drawVisibleGrid(canvas);
}

在XPhotoView調(diào)用draw()方法后栏赴,最后會(huì)進(jìn)行有效性檢查蘑斧,就是checkOrUpdateViewRect(),這時(shí)會(huì)把真正的視圖的大小作為參數(shù)傳給 initiateViewRect()须眷,然后再真正的進(jìn)行參數(shù)的初始化竖瘾。
是的,這里我們用延遲的方式來獲取到真正的視圖大小花颗,雖然代碼不容易理解捕传,但是穩(wěn)定性提高了。

接下來我們要初始化幾個(gè)參數(shù)扩劝,

mShowBitmapRect, mViewBitmapRect, mSampleSize

圖片的初始化顯示方式有幾種庸论,按Android的定義有FIT_CENTER,CENTER_CROP等职辅,這里我們默認(rèn)用 CENTER_CROP 的方式,而且顯示圖片的起始部分聂示,橫圖從左邊開始域携,豎圖從最上面開始。
這里需要關(guān)注的關(guān)鍵代碼是鱼喉,

    private void initiateViewRect(int viewWidth, int viewHeight) {
      ····
      /** 以 view 寬/長比例和 image 寬/長比例做比較
         *  iW/iH < vW/vH : 左右留空秀鞭,取高比值
         *  iW/iH > vW/vH : 上下留空,取寬比值 */
        float ratio = (imgWidth/imgHeight < viewWidth/viewHeight) ? (imgHeight * 1.0f / viewHeight) : (imgWidth * 1.0f / viewWidth);

        mShowBitmapRect.set(0, 0, (int) (imgWidth / ratio), (int) (imgHeight / ratio));

        /** 保存初始大小 */
        mShowBitmapRect.round(mInitiatedShowBitmapRect);

        /** 取縮小到適配view時(shí)的bitmap的起始位置 */
        int left = (int) ((mShowBitmapRect.width() - mViewRect.width()) / 2);
        int top = (int) ((mShowBitmapRect.height() - mViewRect.height()) / 2);

        left = mShowBitmapRect.width() < mViewRect.width() ? left : 0;
        int right = left + mViewRect.width();
        top = mShowBitmapRect.height() < mViewRect.height() ? top : 0;
        int bottom = top + mViewRect.height();

        mViewBitmapRect.set(left, top, right, bottom);
      ····
    }

我們通過比較View和Image的高/寬比例扛禽,確定圖片是橫圖還是豎圖锋边,
以橫圖為例子,此時(shí)我們需要將它上下?lián)未蟮絼偤娩仢M屏幕编曼,那么就得以View的高度和Image的高度來算出壓縮值 ratio豆巨,用它來算出壓縮后的圖片的實(shí)際大小,保存在 mShowBitmapRect中灵巧。
完成這一步后搀矫,接下來計(jì)算mViewBitmapRect。還記得我們的設(shè)定是顯示圖片的起始部位么刻肄,

/** 取縮小到適配view時(shí)的bitmap的起始位置 */
int left = (int) ((mShowBitmapRect.width() - mViewRect.width()) / 2);
int top = (int) ((mShowBitmapRect.height() - mViewRect.height()) / 2);

left = mShowBitmapRect.width() < mViewRect.width() ? left : 0;
int right = left + mViewRect.width();
top = mShowBitmapRect.height() < mViewRect.height() ? top : 0;
int bottom = top + mViewRect.height();

mViewBitmapRect.set(left, top, right, bottom);

這部分代碼瓤球,既保證了超大圖在縮放后從起始位置開始,也保證了普通圖片縮放后不滿屏的情況下居中顯示敏弃,大家可以琢磨琢磨卦羡。

要留意initiateViewRect這個(gè)方法,它不僅在初始化的時(shí)候調(diào)用麦到,后面每次的縮放绿饵,都需要調(diào)用它來更新mShowBitmapRect,因?yàn)槊看蔚目s放都會(huì)讓實(shí)際顯示的圖片大小發(fā)生改變瓶颠。

繪制流程

關(guān)注 draw()方法拟赊,這里是繪制流程的關(guān)鍵部分,
每次繪制前都會(huì)先更新當(dāng)前的各個(gè)Rect對(duì)象粹淋,以獲得對(duì)應(yīng)的顯示中的Grid吸祟,我們只繪制顯示出來的部分Grid單元,
下面的圖大致描述在繪制時(shí)的情形桃移,

* +--+--+--+--+--+
* |XX|XX|11|11|XX|
* +--+--+--+--+--+
* |XX|XX|11|11|XX|
* +--+--+--+--+--+
* |XX|XX|XX|XX|XX|
* +--+--+--+--+--+

標(biāo)記為11的四個(gè)格子屋匕,表示目前可見的區(qū)域,xx的表示不可見區(qū)域借杰。
對(duì)于可見區(qū)域过吻,會(huì)結(jié)合當(dāng)前的縮放值,從mBitmapGrid中取出蔗衡,然后通過XPhotoView傳進(jìn)來的Canvas對(duì)象繪制纤虽,
對(duì)于不可見區(qū)域乳绕,會(huì)回收掉對(duì)應(yīng)的bitmap對(duì)象,以節(jié)省內(nèi)存廓推。

手勢(shì)響應(yīng)

關(guān)于手勢(shì)響應(yīng)刷袍,是比較簡單的一個(gè)部分,
我們定義了GestureManager樊展,把XPhotoView的事件交給GestureManager的onTouchEvent()處理,
這部分代碼相對(duì)簡單堆生,不做過多解釋专缠。

兼容動(dòng)圖

動(dòng)圖的顯示方式有兩種方案,

  • 用Movie類來顯示
  • 托管給Glide的GifDrawable去渲染

Movie的方式

這種方式相對(duì)簡單淑仆,
在我們不知道對(duì)應(yīng)的文件或者圖片是否是動(dòng)圖的情況下涝婉,以正常邏輯設(shè)置即可,
設(shè)置之后會(huì)用Movie類來判斷是否是一個(gè)有效的GIF圖蔗怠,
之后在draw時(shí)墩弯,在Gif的情況下會(huì)用Movie類來進(jìn)行渲染

Glide的方式

很多項(xiàng)目會(huì)用Glide來做圖片的下載和顯示,
Glide本身會(huì)判斷圖片是否為Gif寞射,當(dāng)是Gif時(shí)會(huì)構(gòu)造一個(gè) GifDrawable 對(duì)象渔工,
我們直接把這個(gè)GifDrawable對(duì)象用 setImageDrawable 的方式設(shè)置到XPhotoView,
GifDrawable會(huì)接管動(dòng)圖的繪制流程桥温。
注意如果這種情況下動(dòng)圖不動(dòng)的話引矩,需要在 setImageDrawable 之后調(diào)用 GifDrawable 的 start()方法,

if(glideDrawable instanceof GifDrawable) {
  holder.photoView.setGif(true);
  holder.photoView.setImageDrawable(glideDrawable);
  ((GifDrawable) glideDrawable).start();
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末侵浸,一起剝皮案震驚了整個(gè)濱河市旺韭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌掏觉,老刑警劉巖区端,帶你破解...
    沈念sama閱讀 216,843評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異澳腹,居然都是意外死亡织盼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門遵湖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悔政,“玉大人,你說我怎么就攤上這事延旧∧惫” “怎么了?”我有些...
    開封第一講書人閱讀 163,187評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵迁沫,是天一觀的道長芦瘾。 經(jīng)常有香客問我捌蚊,道長,這世上最難降的妖魔是什么近弟? 我笑而不...
    開封第一講書人閱讀 58,264評(píng)論 1 292
  • 正文 為了忘掉前任缅糟,我火速辦了婚禮,結(jié)果婚禮上祷愉,老公的妹妹穿的比我還像新娘窗宦。我一直安慰自己,他們只是感情好二鳄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評(píng)論 6 390
  • 文/花漫 我一把揭開白布赴涵。 她就那樣靜靜地躺著,像睡著了一般订讼。 火紅的嫁衣襯著肌膚如雪髓窜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,231評(píng)論 1 299
  • 那天欺殿,我揣著相機(jī)與錄音寄纵,去河邊找鬼。 笑死脖苏,一個(gè)胖子當(dāng)著我的面吹牛程拭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播帆阳,決...
    沈念sama閱讀 40,116評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼哺壶,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了蜒谤?” 一聲冷哼從身側(cè)響起山宾,我...
    開封第一講書人閱讀 38,945評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鳍徽,沒想到半個(gè)月后资锰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,367評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡阶祭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評(píng)論 2 333
  • 正文 我和宋清朗相戀三年绷杜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片濒募。...
    茶點(diǎn)故事閱讀 39,754評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鞭盟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瑰剃,到底是詐尸還是另有隱情齿诉,我是刑警寧澤,帶...
    沈念sama閱讀 35,458評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站粤剧,受9級(jí)特大地震影響歇竟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜抵恋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評(píng)論 3 327
  • 文/蒙蒙 一焕议、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧弧关,春花似錦盅安、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至茸习,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間壁肋,已是汗流浹背号胚。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浸遗,地道東北人猫胁。 一個(gè)月前我還...
    沈念sama閱讀 47,797評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像跛锌,于是被迫代替她去往敵國和親弃秆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評(píng)論 2 354