本文來自於騰訊bugly開發者社區,非經做者贊成,請勿轉載,原文地址:http://dev.qq.com/topic/57c7ff5d53bbcffd68c64411java
做者:黃進——QQ音樂團隊android
相信每個Android開發者,在接觸「Hello World」的時候,就造成了一個觀念:Android UI佈局是經過layout目錄下的XML文件定義的。使用XML定義佈局的方式,有着結構清晰、可預覽等優點,於是極爲通用。但是,恰恰在某些場景下,佈局是須要根據運行時的狀態變化的,沒法使用XML預先定義。這時候,咱們只能經過JavaCode控制,在程序運行時,動態的實現對應的佈局。git
因此,做爲入門,將從給三個方面給你們介紹一些動態佈局相關的基礎知識和經驗。github
NinePatchChunk
,解析如何實現後臺下發.9圖片給客戶端使用。這一步,顧名思義,就是把咱們要的View添加到界面上去。這是動態佈局中最基礎最經常使用的步驟。數組
Android開發中,咱們用到的Button
、ImageView
、RelativeLayout
、LinearLayout
等等元素最終都是繼承於View
這個類的。按照我本身的理解,能夠將它們分爲兩類,控件和容器(這兩個名字純屬做者本身編的,並不是官方定義)。Button
、ImageView
這類直接繼承於View
的就是控件,控件通常是用來呈現內容和與用戶交互的;RelativeLayout
、LinearLayout
這類繼承於ViewGroup
的就是容器,容器就是用來裝東西的。Android是嵌套式佈局的設計,所以,容器裝的既能夠是容器,也能夠是控件。微信
更直接的,仍是經過一段demo代碼來看吧。app
首先,由於不能setContentView(R.layout.xxx)
了,咱們須要先添加一個root
做爲整個的容器,ide
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
就能夠了。(有興趣的同窗能夠去看看源碼,這裏就不詳解了)
上一節,咱們只是擺脫了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](http://my.oschina.net/asia):id/background" android:drawable="@drawable/background"/> <item android:id="[@android](http://my.oschina.net/asia):id/secondaryProgress" android:drawable="@drawable/secondary_progress"/> <item android:id="[@android](http://my.oschina.net/asia):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
圖片對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
的生成方法呢?
下面就須要從源碼入手了。
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
這三個數組是最關鍵的,可是具體是什麼,就要繼續看源碼了。
/** * 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,有興趣的同窗能夠直接去學習和使用。
參考資料:
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
http://stackoverflow.com/questions/5079868/create-a-ninepatch-ninepatchdrawable-in-runtime
更多精彩內容歡迎關注bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!