天天看點

【騰訊Bugly幹貨分享】Android動态布局入門及NinePatchChunk解密

相信每一個Android開發者,在接觸“Hello World”的時候,就形成了一個觀念:Android UI布局是通過layout目錄下的XML檔案定義的。使用XML定義布局的方式,有着結構清晰、可預覽等優勢,因而極為通用。可是,偏偏在某些場景下,布局是需要根據運作時的狀态變化的,無法使用XML預先定義。這時候,我們隻能通過JavaCode控制,在程式運作時,動态的實作對應的布局。

本文來自于騰訊bugly開發者社群,非經作者同意,請勿轉載,原文位址:http://dev.qq.com/topic/57c7ff5d53bbcffd68c64411

作者:黃進——QQ音樂團隊

擺脫XML布局檔案

是以,作為入門,将從給三個方面給大家介紹一些動态布局相關的基礎知識和經驗。

  • 動态添加view到界面上,擺脫layout檔案夾下的XML檔案。
  • 熟悉Drawable子類,擺脫drawable檔案夾下的XML檔案。
  • 解密

    NinePatchChunk

    ,解析如何實作背景下發.9圖檔給用戶端使用。

動态添加View

這一步,顧名思義,就是把我們要的View添加到界面上去。這是動态布局中最基礎最常用的步驟。

Android開發中,我們用到的

Button

ImageView

RelativeLayout

LinearLayout

等等元素最終都是繼承于

View

這個類的。按照我自己的了解,可以将它們分為兩類,控件和容器(這兩個名字純屬作者自己編的,并非官方定義)。

Button

ImageView

這類直接繼承于

View

的就是控件,控件一般是用來呈現内容和與使用者互動的;

RelativeLayout

LinearLayout

這類繼承于

ViewGroup

的就是容器,容器就是用來裝東西的。Android是嵌套式布局的設計,是以,容器裝的既可以是容器,也可以是控件。

更直接的,還是通過一段demo代碼來看吧。

首先,因為不能

setContentView(R.layout.xxx)

了,我們需要先添加一個

root

作為整個的容器,

RelativeLayout root = new RelativeLayout(this);
 root.setBackgroundColor(Color.WHITE);
 setContentView(root, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
           

然後,我們嘗試在螢幕正中間添加一個按鈕,

Button button1 = new Button(this);
 button1.setId(View.generateViewId());
 button1.setText("Button1");
 button1.setBackgroundColor(Color.RED);
 LayoutParams btnParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
 btnParams.addRule(RelativeLayout.CENTER_IN_PARENT, 1);
 root.addView(button1, btnParams);
           

到這裡可以發現,隻需要三步,就可以添加一個view(以按鈕為例)到相應的容器

root

裡面了,

  • new Button(this)

    ,并初始化控件相關的屬性。
  • 根據

    root

    的類型,

    new LayoutParams

    ,這個參數主要用來描述要添加的

    view

    在容器中的定位資訊,包括高寬,居中對齊,margin等等屬性。特别地,對于上面的例子,相對于父容器居中的實作是,

    btnParams.addRule(RelativeLayout.CENTER_IN_PARENT, 1)

    ,這裡對應XML的代碼則是

    android:centerInParent='true'

  • 最後一步,添加到容器中,

    root.addView(button1, btnParams)

    就行了。

接下來,搞的稍微複雜點,繼續在按鈕的右下方添加一個線性布局,向其中添加一個

TextView

Button

,而且各自占的寬度比例為2:3(對于

android:layout_weight

屬性),demo代碼如下,

// 在按鈕右下方添加一個線性布局
 LinearLayout linearLayout = new LinearLayout(this);
 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
 LayoutParams lParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
 lParams.addRule(RelativeLayout.BELOW, button1.getId());
 lParams.addRule(RelativeLayout.RIGHT_OF, button1.getId());
 root.addView(linearLayout, lParams);

 // 線上性布局中,添加一個TextView和一個Button,寬度按2:3的比例
 TextView textView = new TextView(this);
 textView.setText("TextView");
 textView.setTextSize(28);
 textView.setBackgroundColor(Color.BLUE);
 LinearLayout.LayoutParams tParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT);
 tParams.weight = 2;    // 定義寬度的比例
 linearLayout.addView(textView, tParams);

 Button button2 = new Button(this);
 button2.setText("Button2");
 button2.setBackgroundColor(Color.RED);
 LinearLayout.LayoutParams bParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT);
 bParams.weight = 3; // 定義寬度的比例
 linearLayout.addView(button2, bParams);
           

需要注意的是,上面代碼中的

lParams.addRule(RelativeLayout.BELOW, button1.getId())

XML

對應

android:layout_below

規則如果定義的是一個view相對于另一個view的,一定要初始化另一個view(

button1

)的id不為0,否則規則會失效。通常,為了防止id重複,建議使用系統方法來生成id,也就是第二段代碼中的

button1.setId(View.generateViewId())

最終,這一段代碼執行下來,我們得到的效果就是,

但是,添加view作者也遇到過一個小小坑。

如下圖左邊部分,作者曾經遇到一個場景,需要在

RelativeLayout

右邊添加一個

ImageView

,同時,這個

ImageView

的右邊部分在

RelativeLayout

的外面。

一開始,作者的代碼如下,卻隻能得到上圖右邊的效果,

ImageView imageView = new ImageView(this);
 RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(width, height);
 params.leftMargin = x;    // 到左邊的距離
 params.topMargin = y;    // 到上邊的距離
 parent.addView(imageView, params);
           

後來本人猜測,這是因為

onMeasure

onLayout

的時候,受到了

rightMargin

預設為0的限制。

後來,經過本人驗證,要跳過這個坑,加一行

params.rightMargin = -1*width

就可以了。(有興趣的同學可以去看看源碼,這裡就不詳解了)

Drawable子類

上一節,我們隻是擺脫了layout目錄的XML檔案。可是還有一類XML檔案,頻繁的被layout目錄的XML檔案引用,那就是drawable目錄的XML檔案。drawable目錄的下檔案,通常是定義了一些,

selector

shape

等等。可是,考慮到一個場景:

selector

裡面引用的圖檔,不是打包時res目錄的資源,而是背景下發的圖檔呢?類似場景下,我們能不能擺脫這類XML檔案呢?

根據上一節的經驗,要相信,

XML

定義能實作的,Java代碼一定能夠實作。從

drawable

的目錄名就可以看出,不管是

selector

shape

或是其他,總歸都應該是

drawable

。是以,在Java代碼中,總應該有一個

Drawable

的子類來對應他們。下面,就介紹幾個常用的

Drawable

的子類給大家。

StateListDrawable:對應

selector

,主要用來描述按鈕等的點選态。

StateListDrawable selector = new StateListDrawable();
 btnSelectorDrawable.addState(new int[]{android.R.attr.state_pressed}, drawablePress);
 btnSelectorDrawable.addState(new int[]{android.R.attr.state_enabled}, drawableEnabel);
 btnSelectorDrawable.addState(new int[]{android.R.attr.state_selected}, drawableSelected);
 btnSelectorDrawable.addState(new int[]{android.R.attr.state_focused}, drawableFocused);
 btnSelectorDrawable.addState(new int[]{}, drawableNormal);
           

GradientDrawable:對應

漸變色

GradientDrawable drawable = new GradientDrawable();
 drawable.setOrientation(Orientation.TOP_BOTTOM); //定義漸變的方向
 drawable.setColors(colors); //colors為int[],支援2個以上的顔色
           

最後,說一個比較複雜的Drawable,是進度條相關的。

LayerDrawable:對應

Seekbar android:progressDrawable

通常,我們用XML定義一個進度條的ProgressDrawable是這樣的,

<!--ProgressDrawable-->
 <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
     <item android:id="@android:id/background" android:drawable="@drawable/background"/>
     <item android:id="@android:id/secondaryProgress" android:drawable="@drawable/secondary_progress"/>
     <item android:id="@android:id/progress" android:drawable="@drawable/progress"/>
 </layer-list>
           

而對于其中的,

@drawable/progress

@drawable/secondary_progress

也不是普通的drawable,

<!--@drawable/progress 定義-->
 <clip xmlns:android="http://schemas.android.com/apk/res/android"
       android:clipOrientation="horizontal"
       android:drawable="@drawable/progress_drawable"
       android:gravity="left" >
 </clip>
           

也就是說,通過XML要定義進度條的

ProgressDrawable

,我們需要定義多個XML檔案的,還是比較複雜的。那麼JavaCode實作呢?

其實,了解了XML實作的方式,下面的JavaCode就很好了解了。

LayerDrawable layerDrawable = (LayerDrawable) getProgressDrawable();

 //背景
 layerDrawable.setDrawableByLayerId(android.R.id.background, backgroundDrawable);

 //進度條
 ClipDrawable clipProgressDrawable = new ClipDrawable(progressDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL);
 layerDrawable.setDrawableByLayerId(android.R.id.progress, clipProgressDrawable);

 //緩沖進度條
 ClipDrawable clipSecondaryProgressDrawable = new ClipDrawable(secondaryProgressDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL);
 layerDrawable.setDrawableByLayerId(android.R.id.secondaryProgress, clipSecondaryProgressDrawable);
           

更多的

Drawable

的子類,大家可以根據自己需求去官方文檔上查詢就行了。

“蛋疼.9.PNG”

.9.png

圖檔對Android開發來說,都不陌生。通常情況下,我們對于

.9.png

圖檔的使用,隻需要簡單的放到resource目錄下,然後,當做普通圖檔來用就可以了。然而,以本人的經驗,如果要動态下發’.9.png’圖檔給用戶端使用就很蛋疼了。

一開始,當我想當然以為可以直接加載本地

.9.png

圖檔,用的飛起的時候,發現了Android Nine Patch的一個大坑!!!

“說好的自動拉升了???”(隐隐約約感覺到某需求的工作量又少評估了一天。。。。。。。)

通過查閱資料發現,原來,工程裡面用的

.9.png

在打包的時候,經過了

aapt

的處理,成為了一張包含有特殊資訊的

.png

圖檔。而不是直接加載的

.9.png

這種圖檔。

那麼第一個思路就來了(參考引用),首先,我們先對

.9.png

執行一個

aapt

指令。

aapt.exe s -i xx.9.png -o xx.png
           

然後,背景下發這種處理過的

.png

,用戶端通過如下代碼,就可以加載這張圖檔,得到一個有局部拉伸效果的

NinePatchDrawable

了。

Bitmap bitmap = BitmapFactory.decodeFile(filePath);
 NinePatchDrawable npd = new NinePatchDrawable(context.getResource(), bitmap, bitmap.getNinePatchChunk(), new Rect(), null);
           

可是,這個初級方式并不是太完美,每次背景配置新的圖檔,都需要

aapt

處理一遍,背景需要針對iOS和Android區分平台下發不同圖檔。總之,不太科學!那麼有沒有更加徹底的方式呢?

徹底了解

.9.png

回顧

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();
           

後來也在github上找到了一個現成的Library,有興趣的同學可以直接去學習和使用。

參考資料:

http://blog.csdn.net/darkinger/article/details/22801215

https://android.googlesource.com/platform/pac

kages/apps/Gallery2/+/jb-dev/src/com/android/gallery3d/ui/NinePatchChunk.java

https://android.googlesource.com/platform/frameworks/base/+/master/include/androidfw/ResourceTypes.h

https://github.com/Anatolii/NinePatchChunk.

http://stackoverflow.com/questions/5079868/create-a-ninepatch-ninepatchdrawable-in-runtime

更多精彩内容歡迎關注bugly的微信公衆賬号:

騰訊 Bugly是一款專為移動開發者打造的品質監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合并功能幫助開發同學把每天上報的數千條 Crash 根據根因合并分類,每日日報會列出影響使用者數最多的崩潰,精準定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在釋出後快速的了解應用的品質情況,适配最新的 iOS, Android 官方作業系統,鵝廠的工程師都在使用,快來加入我們吧!