Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android動態布局入門及NinePatchChunk解密

Android動態布局入門及NinePatchChunk解密

編輯:關於Android編程

擺脫XML布局文件

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

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

動態添加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的外面。

一開始,作者的代碼如下,卻只能得到上圖右邊的效果,<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> 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是這樣的,



    
    
    

而對於其中的,@drawable/progress和@drawable/secondary_progress也不是普通的drawable,




也就是說,通過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,有興趣的同學可以直接去學習和使用。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved