【圖像格式篇】可以從網(wǎng)絡(luò)加載點(diǎn)9圖的嗎?

從網(wǎng)絡(luò)加載點(diǎn)9圖.png

你拿手機(jī)刷著刷著,突然手滑點(diǎn)開(kāi)一張圖,
這圖向上無(wú)限高,向下無(wú)限深,向左無(wú)限遠(yuǎn),向右無(wú)限遠(yuǎn),
這圖是什么?

是點(diǎn)9圖。??

大家好,我是來(lái)顛覆你對(duì)點(diǎn)9圖固有認(rèn)知的星際碼仔。

點(diǎn)9圖幾乎在每個(gè)Android工程中都或多或少地有用到,而切點(diǎn)9圖也可以說(shuō)是每個(gè)Android開(kāi)發(fā)者必備的傳統(tǒng)藝能了,但今天我們要分享的主題估計(jì)各位平時(shí)比較少接觸到,就是——從網(wǎng)絡(luò)加載點(diǎn)9圖

為了講好這個(gè)主題,我們會(huì)從點(diǎn)9圖的基礎(chǔ)知識(shí)出發(fā),比較網(wǎng)絡(luò)加載方式與常規(guī)用法的區(qū)別,然后分別給出一個(gè)次優(yōu)級(jí)和更優(yōu)級(jí)的解決思路,可以根據(jù)你們當(dāng)前項(xiàng)目的實(shí)際情況自由選取。

照例,先給出一張思維導(dǎo)圖,方便復(fù)習(xí):

從網(wǎng)絡(luò)加載點(diǎn)9圖.png

點(diǎn)9圖的基礎(chǔ)知識(shí)

點(diǎn)9圖,官方的正式名稱(chēng)為9-patch,是一種可拉伸的位圖圖像格式,因其必須以.9.png為擴(kuò)展名進(jìn)行保存而得名,通常被用作各類(lèi)視圖控件的背景。

其典型的一個(gè)應(yīng)用就是IM中的聊天氣泡框,氣泡框的寬高會(huì)隨著我們輸入文本的長(zhǎng)短而自適應(yīng)拉伸,但氣泡框資源本身并不會(huì)因拉伸而失真。

聊天氣泡框.jpg

這么神奇的效果是怎么實(shí)現(xiàn)的呢?

答案是:四條黑線。

忽略掉.9.png的擴(kuò)展名,點(diǎn)9圖的本質(zhì)其實(shí)就是一張標(biāo)準(zhǔn)的PNG格式圖片,而與其他普通PNG格式圖片的不同之處在于,點(diǎn)9圖在其圖片的四周額外包含了1像素寬的黑色邊框,用于定義圖片的可拉伸的區(qū)域與可繪制的區(qū)域,以實(shí)現(xiàn)根據(jù)視圖內(nèi)容自動(dòng)調(diào)整圖片大小的效果

可拉伸區(qū)域的定義

可拉伸區(qū)域由左側(cè)及頂部一條或多條黑線來(lái)定義,左側(cè)的黑色邊框定義了縱向拉伸的區(qū)域,頂部的黑色邊框定義了橫向拉伸的區(qū)域,拉伸的效果是通過(guò)復(fù)制區(qū)域內(nèi)圖片的像素來(lái)實(shí)現(xiàn)的。

可拉伸區(qū)域.png

可以看到,由于可拉伸區(qū)域選擇的都是比較平整的區(qū)域,而沒(méi)有覆蓋到四周的圓角,因此圖片無(wú)論怎么縱向或橫向拉伸,四周的圓角都不會(huì)因此而變形失真。

可繪制區(qū)域的定義

可繪制區(qū)域由右側(cè)及底部的各一條黑線來(lái)定義,稱(chēng)為內(nèi)邊距線。如果沒(méi)有添加內(nèi)邊距線,視圖內(nèi)容將默認(rèn)填滿整個(gè)視圖區(qū)域。

沒(méi)有添加內(nèi)邊距線.png

而如果添加了內(nèi)邊距線,則視圖內(nèi)容僅會(huì)在右側(cè)及底部的黑線所定義的區(qū)域內(nèi)顯示,如果視圖內(nèi)容顯示不下,則圖片會(huì)拉伸至合適的尺寸。

添加了內(nèi)邊距線.png

Glide能處理點(diǎn)9圖嗎

點(diǎn)九圖的常規(guī)用法,就是以.9.png為擴(kuò)展名保存在項(xiàng)目的 res/drawable/ 目錄下,并隨著項(xiàng)目一起打包到 *.apk 文件中,然后跟其他普通的PNG格式圖片一樣正常使用即可。

但這種情況在改成了從網(wǎng)絡(luò)加載點(diǎn)9圖之后有所變化。

問(wèn)題在于,即使強(qiáng)大如Glide,對(duì)于從網(wǎng)絡(luò)加載點(diǎn)9圖的這種場(chǎng)景,也沒(méi)有做很好的適配,以至于我們加載完圖片之后會(huì)發(fā)現(xiàn)...

完!全!沒(méi)!有!拉!伸!效!果!

焯.gif

要理解這背后的原因,我們需要把目光轉(zhuǎn)移到一個(gè)原本在打包過(guò)程中常常被我們忽視的角色——AAPT。

AAPT是什么?

AAPT即Android Asset Packaging Tool,是用于構(gòu)建*.apk文件的Android資源打包工具,默認(rèn)存放在Android SDK的build-tools目錄下。

盡管我們很少直接使用AAPT工具,但其卻是.apk文件打包流程中不可或缺的重要一環(huán),具體可參照下面的.apk文件詳細(xì)構(gòu)建流程圖。

*.apk文件詳細(xì)構(gòu)建流程圖.png

流程里,AAPT工具最重要的功能,就是獲取并編譯我們應(yīng)用的資源文件,例如AndroidManifest.xml清單文件和Activity的XML布局文件。 還有就是生成了一個(gè)R.java,以便我們從 Java 代碼中根據(jù)id索引到對(duì)應(yīng)的資源。

而常規(guī)用法下的點(diǎn)9圖之所以能正常工作,也離不開(kāi)打包時(shí),AAPT對(duì)于包含點(diǎn)9圖在內(nèi)的PNG格式圖片的預(yù)處理。

那么,AAPT的預(yù)處理具體都做了哪些事情呢?

AAPT對(duì)點(diǎn)九圖做的預(yù)處理

首先,我們要了解的是,在Android的世界里,存在著兩種不同形式的點(diǎn)9圖文件,分別是“源類(lèi)型(source)”和“已編譯類(lèi)型(compiled)”。

源類(lèi)型就是前面所提到的,使用了包括Draw 9-patch在內(nèi)的點(diǎn)9圖制作工具所創(chuàng)建的、四周帶有1像素寬黑色邊框的PNG圖片。

ic_bubble_right.9.png

而已編譯類(lèi)型指的是,把之前定義好的點(diǎn)九圖數(shù)據(jù)(可拉伸區(qū)域&可繪制區(qū)域等)寫(xiě)入原先格式的輔助數(shù)據(jù)塊后,把四周的黑色邊框抹除了的PNG圖片。

ic_bubble_right.png

這里稍微提一下PNG圖片的文件格式。

Png文件結(jié)構(gòu).png

在文件頭之外,PNG圖片使用了基于“塊(chunk)”的存儲(chǔ)結(jié)構(gòu),每個(gè)塊負(fù)責(zé)傳達(dá)有關(guān)圖像的某些信息。

塊有關(guān)鍵塊輔助塊兩種類(lèi)型,關(guān)鍵塊包含了讀取和渲染PNG文件所需的信息,必不可少。而輔助數(shù)據(jù)塊則是可選的,程序在遇到它不理解的輔助塊時(shí),可以安全地忽略它,這種設(shè)計(jì)可以保持與舊版本的兼容性

點(diǎn)九圖數(shù)據(jù)所放入的,正是一個(gè)tag為“npTc”的輔助數(shù)據(jù)塊。

AAPT在打包過(guò)程中對(duì)點(diǎn)9圖的預(yù)處理,其實(shí)就是將點(diǎn)9圖從源類(lèi)型轉(zhuǎn)換為已編譯類(lèi)型的過(guò)程,也只有已編譯類(lèi)型的點(diǎn)9圖才能被Android系統(tǒng)識(shí)別并處理,從而達(dá)到根據(jù)視圖內(nèi)容自動(dòng)調(diào)整圖片大小的效果。

而直接從網(wǎng)絡(luò)加載的點(diǎn)9圖則缺少這個(gè)過(guò)程,我們實(shí)際拿到的是沒(méi)有經(jīng)過(guò)AAPT預(yù)處理的源類(lèi)型,Android系統(tǒng)就只會(huì)把它當(dāng)普通的PNG格式圖片一樣處理,因此展示時(shí)會(huì)有殘留在四周的黑色邊框,并且當(dāng)視圖內(nèi)容過(guò)大時(shí),圖片就會(huì)因?yàn)椴缓侠砝於a(chǎn)生明顯的失真。

四周殘留黑線.jpg

明白了這一層的原理之后,我們也就有了一個(gè)次優(yōu)級(jí)別的解決思路,也即:

用AAPT命令行還原對(duì)點(diǎn)9圖的預(yù)處理

AAPT同時(shí)也是一個(gè)命令行工具,其在打包過(guò)程中參與的多項(xiàng)工作都可以通過(guò)命令行來(lái)實(shí)現(xiàn)。

其中就包括對(duì)PNG格式圖片的預(yù)處理。

于是,具體可操作的步驟也很清晰了:

步驟1:設(shè)計(jì)組產(chǎn)出源類(lèi)型的點(diǎn)9圖后,即利用AAPT工具轉(zhuǎn)換為已編譯類(lèi)型

這樣做還有一個(gè)好處就是,AAPT命令行工具會(huì)校驗(yàn)源類(lèi)型點(diǎn)9圖的規(guī)格,如果不合規(guī)就會(huì)報(bào)錯(cuò)并給出原因提示,這樣就可以在生產(chǎn)端時(shí)就保證產(chǎn)出點(diǎn)9圖的合規(guī)性,而不是等到展示的時(shí)候才發(fā)現(xiàn)有問(wèn)題。

命令行如下:

 aapt s[ingleCrunch] [-v] -i inputfile -o outputfile

[]表示是可選的完整命令或參數(shù)。

步驟2:交付到資源上傳平臺(tái)后,后端改由下發(fā)這種已編譯類(lèi)型的點(diǎn)9圖

這個(gè)過(guò)程還需保證不會(huì)因流量壓縮而將圖片轉(zhuǎn)為Webp格式,或者造成“npTc”的輔助數(shù)據(jù)塊丟失。

步驟3:客戶端拿到后還需一些額外的處理,以正常識(shí)別和展示點(diǎn)9圖

這里主要涉及到2個(gè)問(wèn)題:

  1. 我們?cè)趺粗老掳l(fā)的資源是已編譯類(lèi)型的點(diǎn)9圖?
  2. 我們?cè)趺锤嬖V系統(tǒng)以點(diǎn)9圖的形式正確處理這張圖?

這2個(gè)問(wèn)題都可以從Android SDK源碼中找到答案。

關(guān)于問(wèn)題1,我們可以從點(diǎn)9圖的常見(jiàn)應(yīng)用場(chǎng)景,即設(shè)為視圖控件背景的API入手,從View#setBackground方法一路深入直至BitmapFactory#setDensityFromOptions方法,就可以看到:

    private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
            ...
            byte[] np = outputBitmap.getNinePatchChunk();
            final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
           ...
    }

Bitmap#getNinePatchChunk方法返回的是一個(gè)byte數(shù)組類(lèi)型的數(shù)據(jù),從方法名就可以看出其正是關(guān)于點(diǎn)九圖規(guī)格的輔助塊數(shù)據(jù):

    public byte[] getNinePatchChunk() {
        return mNinePatchChunk;
    }

NinePatch#isNinePatchChunk方法是一個(gè)Native函數(shù),我們等到后面深入點(diǎn)九圖Native層結(jié)構(gòu)體時(shí)再展開(kāi)講:

    public native static boolean isNinePatchChunk(byte[] chunk);

而關(guān)于問(wèn)題2,我們可以通過(guò)查找對(duì)Bitmap#getNinePatchChunk方法的引用,在Drawable#createFromResourceStream方法中找到一個(gè)參考例子:

    public static Drawable createFromResourceStream(@Nullable Resources res,
            @Nullable TypedValue value, @Nullable InputStream is, @Nullable String srcName,
            @Nullable BitmapFactory.Options opts) {
        ...
        Rect pad = new Rect();
        ...
        Bitmap  bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts);
        if (bm != null) {
            byte[] np = bm.getNinePatchChunk();
            if (np == null || !NinePatch.isNinePatchChunk(np)) {
                np = null;
                pad = null;
            }

            final Rect opticalInsets = new Rect();
            bm.getOpticalInsets(opticalInsets);
            return drawableFromBitmap(res, bm, np, pad, opticalInsets, srcName);
        }
        return null;
    }
    private static Drawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np,
            Rect pad, Rect layoutBounds, String srcName) {

        if (np != null) {
            return new NinePatchDrawable(res, bm, np, pad, layoutBounds, srcName);
        }

        return new BitmapDrawable(res, bm);
    }

可以看到,它是通過(guò)在判斷NinePatchChunk數(shù)據(jù)不為空后,構(gòu)建了一個(gè)NinePatchDrawable來(lái)告訴系統(tǒng)以點(diǎn)9圖的形式正確處理這張圖的。

于是我們可以得出結(jié)論,客戶端要做的額外處理,就是在拿到已編譯類(lèi)型的點(diǎn)9圖并構(gòu)建為Bitmap后:

  1. 先調(diào)用Bitmap#getNinePatchChunk方法嘗試獲取點(diǎn)9圖數(shù)據(jù)

  2. 再通過(guò)NinePatch#isNinePatchChunk方法判斷是不是點(diǎn)9圖數(shù)據(jù)。

  3. 如果是點(diǎn)9圖數(shù)據(jù),則利用這個(gè)點(diǎn)9圖數(shù)據(jù)構(gòu)建一個(gè)NinePatchDrawable

  4. 如果不是,則構(gòu)建一個(gè)BitmapDrawable。

示例代碼如下:

        Glide.with(context).asBitmap().load(url)
            .into(object : CustomTarget<Bitmap>(){
                override fun onResourceReady(bitmap: Bitmap, transition: Transition<in Bitmap>?) {
                    try {
                        val chunk = bitmap.ninePatchChunk
                        val drawable = if (NinePatch.isNinePatchChunk(chunk)) {
                            NinePatchDrawable(context.resources, bitmap, chunk, Rect(), null)
                        } else {
                            BitmapDrawable(context.resources, bitmap);
                        }
                        view.background = drawable;
                    } catch (e: Exception) {
                        e.printStackTrace();
                    }
                }
    
                override fun onLoadCleared(placeholder: Drawable?) {
                }
    
            })

這樣就滿足了嗎?并沒(méi)有。方案本身雖然可行,但讓一向習(xí)慣可視化界面操作的設(shè)計(jì)組同事執(zhí)行命令行,實(shí)在是有點(diǎn)太為難他們了,并且每次產(chǎn)出資源后都要用AAPT工具處理一遍,也確實(shí)有點(diǎn)麻煩。

話說(shuō)回來(lái),命令行工具的底層肯定還是依賴(lài)代碼來(lái)實(shí)現(xiàn)的,那有沒(méi)有可能在客戶端側(cè)實(shí)現(xiàn)一套與AAPT工具一樣的邏輯呢?這就引出了我們一個(gè)更次優(yōu)級(jí)別的解決思路,也即:

在客戶端側(cè)還原對(duì)點(diǎn)9圖的預(yù)處理

透過(guò)上一個(gè)方案我們可以了解到,最關(guān)鍵的地方還是那個(gè)byte數(shù)組類(lèi)型的點(diǎn)九圖數(shù)據(jù)塊(NineChunk),如果我們能知道這個(gè)數(shù)據(jù)塊里面實(shí)際包含什么內(nèi)容,就有機(jī)會(huì)在在客戶端側(cè)構(gòu)造出一份類(lèi)似的數(shù)據(jù)。

上一個(gè)方案中提到的NinePatch#isNinePatchChunk方法就是我們的突破點(diǎn)。

接下來(lái),就讓我們進(jìn)入Native層查看isNinePatchChunk方法的源碼實(shí)現(xiàn)吧:

    static jboolean isNinePatchChunk(JNIEnv* env, jobject, jbyteArray obj) {
        if (NULL == obj) {
            return JNI_FALSE;
        }
        if (env->GetArrayLength(obj) < (int)sizeof(Res_png_9patch)) {
            return JNI_FALSE;
        }
        const jbyte* array = env->GetByteArrayElements(obj, 0);
        if (array != NULL) {
            const Res_png_9patch* chunk = reinterpret_cast<const Res_png_9patch*>(array);
            int8_t wasDeserialized = chunk->wasDeserialized;
            env->ReleaseByteArrayElements(obj, const_cast<jbyte*>(array), JNI_ABORT);
            return (wasDeserialized != -1) ? JNI_TRUE : JNI_FALSE;
        }
        return JNI_FALSE;
    }

可以看到,在isNinePatchChunk方法內(nèi)部實(shí)際是將傳入的byte數(shù)組類(lèi)型的點(diǎn)9圖數(shù)據(jù)轉(zhuǎn)為一個(gè)Res_png_9patch類(lèi)型的結(jié)構(gòu)體,再通過(guò)一個(gè)wasDeserialized的結(jié)構(gòu)變量來(lái)判斷是不是點(diǎn)9圖數(shù)據(jù)的。

這個(gè)Res_png_9patch類(lèi)型的結(jié)構(gòu)體內(nèi)部是這樣的:

 * This chunk specifies how to split an image into segments for
 * scaling.
 *
 * There are J horizontal and K vertical segments.  These segments divide
 * the image into J*K regions as follows (where J=4 and K=3):
 *
 *      F0   S0    F1     S1
 *   +-----+----+------+-------+
 * S2|  0  |  1 |  2   |   3   |
 *   +-----+----+------+-------+
 *   |     |    |      |       |
 *   |     |    |      |       |
 * F2|  4  |  5 |  6   |   7   |
 *   |     |    |      |       |
 *   |     |    |      |       |
 *   +-----+----+------+-------+
 * S3|  8  |  9 |  10  |   11  |
 *   +-----+----+------+-------+
 *
 * Each horizontal and vertical segment is considered to by either
 * stretchable (marked by the Sx labels) or fixed (marked by the Fy
 * labels), in the horizontal or vertical axis, respectively. In the
 * above example, the first is horizontal segment (F0) is fixed, the
 * next is stretchable and then they continue to alternate. Note that
 * the segment list for each axis can begin or end with a stretchable
 * or fixed segment.
 * /
struct alignas(uintptr_t) Res_png_9patch
{
    Res_png_9patch() : wasDeserialized(false), xDivsOffset(0),
                       yDivsOffset(0), colorsOffset(0) { }

    int8_t wasDeserialized;
    uint8_t numXDivs;
    uint8_t numYDivs;
    uint8_t numColors;

    // The offset (from the start of this structure) to the xDivs & yDivs
    // array for this 9patch. To get a pointer to this array, call
    // getXDivs or getYDivs. Note that the serialized form for 9patches places
    // the xDivs, yDivs and colors arrays immediately after the location
    // of the Res_png_9patch struct.
    uint32_t xDivsOffset;
    uint32_t yDivsOffset;

    int32_t paddingLeft, paddingRight;
    int32_t paddingTop, paddingBottom;

    enum {
        // The 9 patch segment is not a solid color.
        NO_COLOR = 0x00000001,

        // The 9 patch segment is completely transparent.
        TRANSPARENT_COLOR = 0x00000000
    };

    // The offset (from the start of this structure) to the colors array
    // for this 9patch.
    uint32_t colorsOffset;
    ...

    inline int32_t* getXDivs() const {
        return reinterpret_cast<int32_t*>(reinterpret_cast<uintptr_t>(this) + xDivsOffset);
    }
    inline int32_t* getYDivs() const {
        return reinterpret_cast<int32_t*>(reinterpret_cast<uintptr_t>(this) + yDivsOffset);
    }
    inline uint32_t* getColors() const {
        return reinterpret_cast<uint32_t*>(reinterpret_cast<uintptr_t>(this) + colorsOffset);
    }
} __attribute__((packed));

很明顯,這個(gè)結(jié)構(gòu)體就是用來(lái)存儲(chǔ)點(diǎn)9圖規(guī)格數(shù)據(jù)的,我們可以根據(jù)該結(jié)構(gòu)體的源碼和注釋梳理出每個(gè)變量的含義:

每個(gè)變量的含義.png

根據(jù)該結(jié)構(gòu)體注釋中的描述,這個(gè)結(jié)構(gòu)體是用于指定如何將圖像分割成多個(gè)部分以進(jìn)行縮放的,其中:

  • Sx標(biāo)簽標(biāo)記的是拉伸區(qū)域(stretchable),F(xiàn)x標(biāo)簽標(biāo)記的是固定區(qū)域(fixed)
  • mDivX描述了所有S區(qū)域水平方向的起始位置和結(jié)束位置
  • mDivY描述了所有S區(qū)域垂直方向的起始位置和結(jié)束位置
  • mColor描述了每個(gè)小區(qū)域的顏色

以該結(jié)構(gòu)體注釋中的例子來(lái)說(shuō),mDivX,mDivY,mColor分別如下:

 *      F0   S0    F1     S1
 *   +-----+----+------+-------+
 * S2|  0  |  1 |  2   |   3   |
 *   +-----+----+------+-------+
 *   |     |    |      |       |
 *   |     |    |      |       |
 * F2|  4  |  5 |  6   |   7   |
 *   |     |    |      |       |
 *   |     |    |      |       |
 *   +-----+----+------+-------+
 * S3|  8  |  9 |  10  |   11  |
 *   +-----+----+------+-------+
mDivX = [ S0.start, S0.end, S1.start, S1.end];
mDivY = [ S2.start, S2.end, S3.start, S3.end];
mColor = [c[0],c[1],...,c[11]]

我畫(huà)了一張示意圖,應(yīng)該會(huì)更方便理解一點(diǎn):

注釋例子示意圖.png

這幾個(gè)結(jié)構(gòu)體變量所描述的,不正是我們?cè)搭?lèi)型的點(diǎn)9圖四周所對(duì)應(yīng)的那些黑色邊框的位置嗎?

那么,現(xiàn)在我們只需要在Java層定義一個(gè)與Res_png_9patch結(jié)構(gòu)體的數(shù)據(jù)結(jié)構(gòu)一模一樣的類(lèi),并在填充關(guān)鍵的變量數(shù)據(jù)后序列化為byte數(shù)組類(lèi)型的數(shù)據(jù),就可以作為NinePatchDrawable構(gòu)造函數(shù)的參數(shù)了。

怎么做呢?這部分有點(diǎn)復(fù)雜,Github上已經(jīng)有一個(gè)大神開(kāi)源出了方案,可以參考下其源碼實(shí)現(xiàn):https://github.com/Anatolii/NinePatchChunk

這里只給出使用層的示例代碼:

     Glide.with(context).asBitmap().load(url)
            .into(object : CustomTarget<Bitmap>(){
                override fun onResourceReady(bitmap: Bitmap, transition: Transition<in Bitmap>?) {
                    try {
                        val drawable = NinePatchChunk.create9PatchDrawable(textBackground.context, resource, null)
                        view.background = drawable;
                    } catch (e: Exception) {
                        e.printStackTrace();
                    }
                }

                override fun onLoadCleared(placeholder: Drawable?) {
                }

            })

NinePatchChunk類(lèi)即為前面說(shuō)的在Java層定義的類(lèi),并提供了幾個(gè)靜態(tài)方法用于創(chuàng)建NinePatchDrawable,其在內(nèi)部會(huì)去檢測(cè)傳入的Bitmap實(shí)例屬于哪種類(lèi)型:

    public static BitmapType determineBitmapType(Bitmap bitmap) {
        if (bitmap == null) return NULL;
        byte[] ninePatchChunk = bitmap.getNinePatchChunk();
        if (ninePatchChunk != null && android.graphics.NinePatch.isNinePatchChunk(ninePatchChunk))
            return NinePatch;
        if (NinePatchChunk.isRawNinePatchBitmap(bitmap))
            return RawNinePatch;
        return PlainImage;
    }

NinePatch即為已編譯類(lèi)型的點(diǎn)9圖,RawNinePatch即為源類(lèi)型的點(diǎn)9圖,源類(lèi)型是通過(guò)PNG圖片4個(gè)角像素是否為透明且是否包含黑色邊框判斷的。

    public static boolean isRawNinePatchBitmap(Bitmap bitmap) {
        if (bitmap == null) return false;
        if (bitmap.getWidth() < 3 || bitmap.getHeight() < 3)
            return false;
        if (!isCornerPixelsAreTrasperent(bitmap))
            return false;
        if (!hasNinePatchBorder(bitmap))
            return false;
        return true;
    }

好了,這個(gè)就是今天要分享的內(nèi)容。最后留給大家一個(gè)問(wèn)題,你覺(jué)得.9.png的擴(kuò)展名對(duì)于從網(wǎng)絡(luò)加載點(diǎn)九圖有影響嗎?

少俠,請(qǐng)留步!若本文對(duì)你有所幫助或啟發(fā),還請(qǐng):

  1. 點(diǎn)贊,讓更多的人能看到!
  2. 收藏?,好文值得反復(fù)品味!
  3. 關(guān)注?,不錯(cuò)過(guò)每一次更文!

你的支持是我繼續(xù)創(chuàng)作的動(dòng)力,感謝!

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,463評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,009評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,218評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,436評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,659評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,917評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容