Android 點九圖機制講解及在聊天氣泡中的應用

點九圖簡介

Android爲了使用同一張圖做爲不一樣數量文字的背景,設計了一種能夠指定區域拉伸的圖片格式「.9.png」,這種圖片格式就是點九圖。java

注意:這種圖片格式只能被使用於Android開發。在ios開發中,能夠在代碼中指定某個點進行拉伸,而在Android中不行,因此在Android中想要達到這個效果,只能使用點九圖(下文會啪啪打臉,實際上是能夠的,只是不多人這樣使用,兼容性不知道怎麼樣,點擊跳轉android

點九圖實質

點九圖的本質其實是在圖片的四周各增長了1px的像素,並使用純黑(#FF000000)的線進行標記,其它的與原圖沒有任何區別。能夠參考如下圖片:ios

標記位置 含義
左-黑點 縱向拉伸區域
上-黑點 橫向拉伸區域
右-黑線 縱向顯示區域
下-黑線 橫向顯示區域

點九圖在 Android 中的應用

點九圖在 Android 中主要有三種應用方式程序員

  1. 直接放在 res 目錄中的 drawable 或者 mipmap 目錄中
  2. 放在 assert 目錄中
  3. 從網絡下載

第一種方式是咱們最經常使用的,直接調用 setBackgroundResource 或者 setImageResource 方法,這樣的話圖片及能夠作到自動拉伸。面試

而對於第二種或者第三種方式,若是咱們直接去加載 .9.png,你會發現圖片或者圖片背景根本沒法拉伸。納尼,這是爲甚麼呢。下面,且聽老衲慢慢道來。算法

Android 並非直接使用點九圖,而是在編譯時將其轉換爲另一種格式,這種格式是將其四周的黑色像素保存至Bitmap類中的一個名爲 mNinePatchChunk 的 byte[] 中,並抹除掉四周的這一個像素的寬度;接着在使用時,若是 Bitmap 的這個 mNinePatchChunk 不爲空,且爲 9patch chunk,則將其構造爲 NinePatchDrawable,不然將會被構造爲 BitmapDrawable,最終設置給 view。數組

所以,在 Android 中,咱們若是想動態使用網絡下載的點九圖,通常須要通過如下步驟:緩存

  1. 使用 sdk 目錄下的 aapt 工具將點九圖轉化爲 png 圖片
  2. 解析圖片的時候,判斷是否含有 NinePatchChunk,有的話,轉化爲 NinePatchDrawable
public static void setNineImagePatch(View view, File file, String url) {
    if (file.exists()) {
        Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
        byte[] chunk = bitmap.getNinePatchChunk();
        if (NinePatch.isNinePatchChunk(chunk)) {
            NinePatchDrawable patchy = new NinePatchDrawable(view.getResources(), bitmap, chunk, new Rect(), null);
            view.setBackground(patchy);
        }
        
    }
}
複製代碼

點九圖上傳服務器流程


aapt 轉換命令

單個圖片文件轉換服務器

./aapt s -i xxx.9.png -o xxx.png
複製代碼

批量轉換微信

# 批量轉換
./aapt c -S inputDir -C outputDir
# inputDir 爲原始.9圖文件夾,outputDir 爲輸出文件夾
複製代碼

執行成功實例

jundeMacBook-Pro:一期氣泡 junxu$ ./aapt c -S /Users/junxu/Desktop/一期氣泡/氣泡需求整理 -C /Users/junxu/Desktop/一期氣泡/output 
Crunching PNG Files in source dir: /Users/junxu/Desktop/一期氣泡/氣泡需求整理
To destination dir: /Users/junxu/Desktop/一期氣泡/output
複製代碼

注意:

若不是標準的點九圖,在轉換的過程會報錯,這時候請設計從新提供新的點九圖


實際開發當中遇到的問題

小屏手機適配問題

剛開始,咱們的切圖是按照 2 倍圖切的,這樣在小屏幕手機上會手機氣泡高度過大的問題。

緣由分析:

該現象的本質是點九圖圖片的高度大於單行文本消息的高度。

解決方案一(暫時不可取):

  1. 我嘗試去壓縮點九圖,但最終再部分手機上面顯示錯亂,不知道是否是壓縮點九圖的方法錯了。

解決方案二

對於低分辨率的手機和高分辨的手機分別下發不一樣的圖片 url,咱們嘗試過得方案是當 density < 2 的時候,採用一倍圖圖片,density >= 2 採用二倍圖圖片。

解決方案三

可能有人會有這樣的疑問呢,爲何要採用一倍圖,兩倍圖的解決方案呢?直接讓 UI 設計師給一套圖,點九圖圖片的高度適中不就解決了。是啊,咱們也是這樣想得,但他們說對於有一些裝飾的點九圖,若是縮小高度,一些裝飾圖案他們不太好切。好比下面圖片中的星星。

小結

說到底,方案二,方案三其實都是折中的一種方案,若是直接可以作到點九圖縮放,那就完美解決了。而 Android 中 res 目錄中的 drawable 或者 mipmap 的點九圖確實能作到,去看了相關的代碼,目前也沒有發現什麼好的解決方案,若是你有好的解決方案話,歡迎留言交流。

點九圖的 padding 在部分手機上面失效

這個是部分 Android 手機的 bug,解決方法見:stackoverflow.com/questions/1…

public class NinePatchChunk {

    private static final String TAG = "NinePatchChunk";

    public final Rect mPaddings = new Rect();

    public int mDivX[];
    public int mDivY[];
    public int mColor[];

    private static float density = IMO.getInstance().getResources().getDisplayMetrics().density;

    private static void readIntArray(final int[] data, final ByteBuffer buffer) {
        for (int i = 0, n = data.length; i < n; ++i)
            data[i] = buffer.getInt();
    }

    private static void checkDivCount(final int length) {
        if (length == 0 || (length & 0x01) != 0)
            throw new IllegalStateException("invalid nine-patch: " + length);
    }

    public static Rect getPaddingRect(final byte[] data) {
        NinePatchChunk deserialize = deserialize(data);
        if (deserialize == null) {
            return new Rect();
        }
    }

    public static NinePatchChunk deserialize(final byte[] data) {
        final ByteBuffer byteBuffer =
                ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());

        if (byteBuffer.get() == 0) {
            return null; // is not serialized
        }

        final NinePatchChunk chunk = new NinePatchChunk();
        chunk.mDivX = new int[byteBuffer.get()];
        chunk.mDivY = new int[byteBuffer.get()];
        chunk.mColor = new int[byteBuffer.get()];

        try {
            checkDivCount(chunk.mDivX.length);
            checkDivCount(chunk.mDivY.length);
        } catch (Exception e) {
            return null;
        }


        // skip 8 bytes
        byteBuffer.getInt();
        byteBuffer.getInt();


        chunk.mPaddings.left = byteBuffer.getInt();
        chunk.mPaddings.right = byteBuffer.getInt();
        chunk.mPaddings.top = byteBuffer.getInt();
        chunk.mPaddings.bottom = byteBuffer.getInt();


        // skip 4 bytes
        byteBuffer.getInt();

        readIntArray(chunk.mDivX, byteBuffer);
        readIntArray(chunk.mDivY, byteBuffer);
        readIntArray(chunk.mColor, byteBuffer);

        return chunk;
    }
}

NinePatchDrawable patchy = new NinePatchDrawable(view.getResources(), bitmap, chunk, NinePatchChunk.getPaddingRect(chunk), null);
view.setBackground(patchy);


複製代碼

動態下載點九圖會致使聊天氣泡閃爍

  1. 這裏咱們採起的方案是預下載(預下載 10 個)
  2. 聊天氣泡採用內存緩存,磁盤緩存,確保 RecyclerView 快速滑動的時候不會閃爍

理解點九圖

如下內容參考騰訊音樂的 Android動態佈局入門及NinePatchChunk解密

回顧NinePatchDrawable的構造方法第三個參數bitmap.getNinePatchChunk(),做者猜測,aapt命令其實就是在bitmap圖片中,加入了NinePatchChunk的信息,那麼咱們是否是隻要能本身構造出這個東西,就可讓任何圖片按照咱們想要的方式拉昇了呢?

但是查了一堆官方文檔,彷佛並找不到相應的方法來得到這個byte[]類型的chunk參數。

既然沒法知道這個chunk如何生成,那麼能不能從解析的角度逆向得出這個NinePatchChunk的生成方法呢?

下面就須要從源碼入手了。

NinePatchChunk.java

public static NinePatchChunk deserialize(byte[] data) {
    ByteBuffer byteBuffer =
            ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
    byte wasSerialized = byteBuffer.get();
    if (wasSerialized == 0) return null;
    NinePatchChunk chunk = new NinePatchChunk();
    chunk.mDivX = new int[byteBuffer.get()];
    chunk.mDivY = new int[byteBuffer.get()];
    chunk.mColor = new int[byteBuffer.get()];
    checkDivCount(chunk.mDivX.length);
    checkDivCount(chunk.mDivY.length);
    // skip 8 bytes
    byteBuffer.getInt();
    byteBuffer.getInt();
    chunk.mPaddings.left = byteBuffer.getInt();
    chunk.mPaddings.right = byteBuffer.getInt();
    chunk.mPaddings.top = byteBuffer.getInt();
    chunk.mPaddings.bottom = byteBuffer.getInt();
    // skip 4 bytes
    byteBuffer.getInt();
    readIntArray(chunk.mDivX, byteBuffer);
    readIntArray(chunk.mDivY, byteBuffer);
    readIntArray(chunk.mColor, byteBuffer);
    return chunk;
}
複製代碼

其實從這部分解析byte[] chunk的源碼,咱們已經能夠反推出來大概的結構了。以下圖,

按照上圖中的猜測以及對.9.png的認識,直覺感覺到,mDivX,mDivY,mColor這三個數組是最關鍵的,可是具體是什麼,就要繼續看源碼了。

ResourceTypes.h

/**
 * 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.
 * /
複製代碼

正如源碼中,註釋的同樣,這個NinePatch Chunk把圖片從x軸和y軸分紅若干個區域,F區域表明了固定,S區域表明了拉伸。mDivX,mDivY描述了全部S區域的位置起始,而mColor描述了,各個Segment的顏色,一般狀況下,賦值爲源碼中定義的NO_COLOR = 0x00000001就好了。就以源碼註釋中的例子來講,mDivX,mDivY,mColor以下:

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]]
複製代碼

對於mColor這個數組,長度等於劃分的區域數,是用來描述各個區域的顏色的,而若是咱們這個只是描述了一個bitmap的拉伸方式的話,是不須要顏色的,即源碼中NO_COLOR = 0x00000001

說了這麼多,咱們仍是經過一個簡單例子來講明如何構造一個按中心點拉伸的 NinePatchDrawable 吧,

Bitmap bitmap = BitmapFactory.decodeFile(filepath);
int[] xRegions = new int[]{bitmap.getWidth() / 2, bitmap.getWidth() / 2 + 1};
int[] yRegions = new int[]{bitmap.getWidth() / 2, bitmap.getWidth() / 2 + 1};
int NO_COLOR = 0x00000001;
int colorSize = 9;
int bufferSize = xRegions.length * 4 + yRegions.length * 4 + colorSize * 4 + 32;

ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize).order(ByteOrder.nativeOrder());
// 第一個byte,要不等於0
byteBuffer.put((byte) 1);

//mDivX length
byteBuffer.put((byte) 2);
//mDivY length
byteBuffer.put((byte) 2);
//mColors length
byteBuffer.put((byte) colorSize);

//skip
byteBuffer.putInt(0);
byteBuffer.putInt(0);

//padding 先設爲0
byteBuffer.putInt(0);
byteBuffer.putInt(0);
byteBuffer.putInt(0);
byteBuffer.putInt(0);

//skip
byteBuffer.putInt(0);

// mDivX
byteBuffer.putInt(xRegions[0]);
byteBuffer.putInt(xRegions[1]);

// mDivY
byteBuffer.putInt(yRegions[0]);
byteBuffer.putInt(yRegions[1]);

// mColors
for (int i = 0; i < colorSize; i++) {
    byteBuffer.putInt(NO_COLOR);
}

return byteBuffer.array();
複製代碼

create-a-ninepatch-ninepatchdrawable-in-runtime

在 stackoverflow 上面也找到牛逼的類,能夠動態建立點九圖,並拉伸圖片,啪啪打臉,剛開始說到 android 中沒法想 ios 同樣動態指定圖片拉伸區域。

public class NinePatchBuilder {
    int width, height;
    Bitmap bitmap;
    Resources resources;
    private ArrayList<Integer> xRegions = new ArrayList<Integer>();
    private ArrayList<Integer> yRegions = new ArrayList<Integer>();

    public NinePatchBuilder(Resources resources, Bitmap bitmap) {
        width = bitmap.getWidth();
        height = bitmap.getHeight();
        this.bitmap = bitmap;
        this.resources = resources;
    }

    public NinePatchBuilder(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public NinePatchBuilder addXRegion(int x, int width) {
        xRegions.add(x);
        xRegions.add(x + width);
        return this;
    }

    public NinePatchBuilder addXRegionPoints(int x1, int x2) {
        xRegions.add(x1);
        xRegions.add(x2);
        return this;
    }

    public NinePatchBuilder addXRegion(float xPercent, float widthPercent) {
        int xtmp = (int) (xPercent * this.width);
        xRegions.add(xtmp);
        xRegions.add(xtmp + (int) (widthPercent * this.width));
        return this;
    }

    public NinePatchBuilder addXRegionPoints(float x1Percent, float x2Percent) {
        xRegions.add((int) (x1Percent * this.width));
        xRegions.add((int) (x2Percent * this.width));
        return this;
    }

    public NinePatchBuilder addXCenteredRegion(int width) {
        int x = (int) ((this.width - width) / 2);
        xRegions.add(x);
        xRegions.add(x + width);
        return this;
    }

    public NinePatchBuilder addXCenteredRegion(float widthPercent) {
        int width = (int) (widthPercent * this.width);
        int x = (int) ((this.width - width) / 2);
        xRegions.add(x);
        xRegions.add(x + width);
        return this;
    }

    public NinePatchBuilder addYRegion(int y, int height) {
        yRegions.add(y);
        yRegions.add(y + height);
        return this;
    }

    public NinePatchBuilder addYRegionPoints(int y1, int y2) {
        yRegions.add(y1);
        yRegions.add(y2);
        return this;
    }

    public NinePatchBuilder addYRegion(float yPercent, float heightPercent) {
        int ytmp = (int) (yPercent * this.height);
        yRegions.add(ytmp);
        yRegions.add(ytmp + (int) (heightPercent * this.height));
        return this;
    }

    public NinePatchBuilder addYRegionPoints(float y1Percent, float y2Percent) {
        yRegions.add((int) (y1Percent * this.height));
        yRegions.add((int) (y2Percent * this.height));
        return this;
    }

    public NinePatchBuilder addYCenteredRegion(int height) {
        int y = (int) ((this.height - height) / 2);
        yRegions.add(y);
        yRegions.add(y + height);
        return this;
    }

    public NinePatchBuilder addYCenteredRegion(float heightPercent) {
        int height = (int) (heightPercent * this.height);
        int y = (int) ((this.height - height) / 2);
        yRegions.add(y);
        yRegions.add(y + height);
        return this;
    }

    public byte[] buildChunk() {
        if (xRegions.size() == 0) {
            xRegions.add(0);
            xRegions.add(width);
        }
        if (yRegions.size() == 0) {
            yRegions.add(0);
            yRegions.add(height);
        }
     
        int NO_COLOR = 1;//0x00000001;
        int COLOR_SIZE = 9;//could change, may be 2 or 6 or 15 - but has no effect on output
        int arraySize = 1 + 2 + 4 + 1 + xRegions.size() + yRegions.size() + COLOR_SIZE;
        ByteBuffer byteBuffer = ByteBuffer.allocate(arraySize * 4).order(ByteOrder.nativeOrder());
        byteBuffer.put((byte) 1);//was translated
        byteBuffer.put((byte) xRegions.size());//divisions x
        byteBuffer.put((byte) yRegions.size());//divisions y
        byteBuffer.put((byte) COLOR_SIZE);//color size

        //skip
        byteBuffer.putInt(0);
        byteBuffer.putInt(0);

        //padding -- always 0 -- left right top bottom
        byteBuffer.putInt(0);
        byteBuffer.putInt(0);
        byteBuffer.putInt(0);
        byteBuffer.putInt(0);

        //skip
        byteBuffer.putInt(0);

        for (int rx : xRegions)
            byteBuffer.putInt(rx); // regions left right left right ...
        for (int ry : yRegions)
            byteBuffer.putInt(ry);// regions top bottom top bottom ...

        for (int i = 0; i < COLOR_SIZE; i++)
            byteBuffer.putInt(NO_COLOR);

        return byteBuffer.array();
    }

    public NinePatch buildNinePatch() {
        byte[] chunk = buildChunk();
        if (bitmap != null)
            return new NinePatch(bitmap, chunk, null);
        return null;
    }

    public NinePatchDrawable build() {
        NinePatch ninePatch = buildNinePatch();
        if (ninePatch != null)
            return new NinePatchDrawable(resources, ninePatch);
        return null;
    }
}

複製代碼

運行一下測試代碼

mLlRoot = findViewById(R.id.ll_root);
try {
    InputStream is = getAssets().open("sea.png");
    Bitmap bitmap = BitmapFactory.decodeStream(is);
    for (int i = 0; i < 5; i++) {
        NinePatchDrawable ninePatchDrawable = NinePatchHelper.buildMulti(this, bitmap);
        TextView textView = new TextView(this);
        textView.setTextSize(25);
        textView.setPadding(20, 10, 20, 10);
        textView.setText(strArray[i]);
        textView.setGravity(Gravity.CENTER_VERTICAL);
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        layoutParams.leftMargin = 20;
        layoutParams.rightMargin = 20;
        textView.setLayoutParams(layoutParams);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            textView.setBackground(ninePatchDrawable);
        }
        mLlRoot.addView(textView);
    }
} catch (IOException e) {
    e.printStackTrace();
}


複製代碼

能夠看到,咱們的圖片完美拉伸


參考文章

  1. cloud.tencent.com/developer/a…
  2. mp.weixin.qq.com/s?__biz=MzI…

推薦閱讀

你們若是以爲不錯的話,能夠關注個人微信公衆號程序員徐公

  1. 公衆號程序員徐公回覆黑馬,獲取 Android 學習視頻
  2. 公衆號程序員徐公回覆徐公666,獲取簡歷模板,教你如何優化簡歷,走進大廠
  3. 公衆號程序員徐公回覆面試,能夠得到面試常見算法,劍指 offer 題解
  4. 公衆號程序員徐公回覆馬士兵,能夠得到馬士兵學習視頻一份

相關文章
相關標籤/搜索