1. 程式人生 > >android view建構函式研究

android view建構函式研究

         上週遇到了SurfaceView的constructor的問題,週末決定略微細緻地研究一下這個令人髮指的玩意。   SurfaceView是View的子類,與View一樣有三個constructor: 1 public void CustomView(Context context{}
2 public void CustomView(Context context, AttributeSet attrs{}
3 public void CustomView(Context context, AttributeSet attrs, int defStyle{}
  為了方便,我們分別命名為C1,C2,C3。   C1是最簡單的一個,如果你只打算用code動態建立一個view而不使用佈局檔案xml inflate,那麼實現C1就可以了。   C2多了一個AttributeSet型別的引數,在通過佈局檔案xml建立一個view時,這個引數會將xml裡設定的屬性傳遞給建構函式。如果你採用xml inflate的方法卻沒有在code裡實現C2,那麼執行時就會報錯。但是由於編譯能順利通過,對於我這樣的菜鳥,這個錯誤有時不太容易被發現。   關於C1和C2,google和度娘上都有很多文章介紹,我就不做贅述。   扯淡的是C3。   C3多了一個defStyle的int引數,關於這個引數doc裡是這樣描述的:   The default style to apply to this view. If 0, no style will be applied (beyond what is included in the theme). This may either be an attribute resource, whose value will be retrieved from the current theme, or an explicit style resource.   從字面上翻譯,這個引數似乎是用來指定view的預設style的,如果是0,那麼將不會應用任何預設(或者叫預設)的style。另外這個引數可以是一個屬性指定的style引用,也可以直接是一個顯式的style資源。   這僅僅是字面上翻譯的結果,就已經不太好理解了。我琢磨了一下,大概有這麼兩個問題: 1. 這個C3什麼時候會被呼叫?   C1是程式碼建立view時,C2是xml建立view時,那麼C3呢?既然defStyle是一個與指定style有關的引數,那麼一個比較自然的猜想是當在程式碼比如xml裡通過某種方式指定了view的style時,C3在該view被inflate時呼叫,並將style傳入給defStyle。   那麼在xml裡指定style有幾種方式呢?大概有兩種,一種是在直接在佈局檔案該view標籤裡使用 1
 style="@style/customstyle"
來指定,另一種是採用指定theme的方式,在AndroidManifest.xml的application標籤裡使用 1 android:theme="@style/customstyle"
這兩種方式都需要在res/values/styles.xml裡定義customstyle: 1 <?xml version="1.0" encoding="utf-8"?>
2 <resources>
3     <style name="customstyle">
4         <item name="android:background"
>@drawable/bg</item>
5         [... or other style code...]
6     </style>
7 </resources>
注:使用theme時標準的theme定義方式是把style放在themes.xml裡而不是styles.xml,但實際上R.java在生成時無論是themes.xml和styles.xml裡的style都是同質的,都存在於R.style下。 回到C3的問題上來,那麼這兩種指定style的方式會不會觸發C3呢?很遺憾,經測試,不會。並且至今我沒發現任何一種情況會自動地(隱式地)呼叫建構函式C3……不知道究竟有沒有這種情況存在呢? 那麼C3到底什麼時候被呼叫呢?答案是當你顯式呼叫它的時候。通常是在C1或者C2裡,用 1
 public void CustomView(Context context, AttributeSet attrs{
2     this(context, attrs, resid);
3 }
的方式將真正建構函式的實現轉移到C3裡,並由resid指定defStyle,作為預設style。比如android原始碼中button的實現:   For example, a Button class's constructor would call this version of the super class constructor and supply R.attr.buttonStyle for defStyle; this allows the theme's button style to modify all of the base view attributes (in particular its background) as well as the Button class's attributes. (摘自C3的doc)這裡的R.attr.buttonStyle就是一個resid。於是引出了第二個問題。 2. defStyle接受什麼樣的值?   你可能會說,doc上不是寫著呢麼?這個引數可以是一個屬性指定的style引用,也可以直接是一個顯式的style資源。   那我們就試驗一下看看。   首先在res/styles.xml裡定義一個style: 1 <?xml version="1.0" encoding="utf-8"?>
2 <resources>
3     <style name="purple">
4         <item name="android:background">#FFFF00FF</item>
5     </style>
6 </resources>
  然後自定義一個View(或者SurfaceView也是可以的): CustomView.java 01 package com.your.test;
02
03 public class CustomView extends View {
04
05     //C1
06     public CustomView(Context context{
07         super(context);
08     }
09
10     //C2
11     public CustomView(Context context, AttributeSet attrs{
12         this(context, attrs, 0);
13     }
14
15     //C3
16     public CustomView(Context context, AttributeSet attrs, int defStyle{
17         super(context, attrs, defStyle);
18     }
19 }
  之後是佈局檔案layout/main.xml: 1 <?xml version="1.0" encoding="utf-8"?>
2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3               xmlns:myxmlns="http://schemas.android.com/apk/res/com.your.test"
4               android:orientation="vertical"
5               android:layout_width="fill_parent"
6               android:layout_height="fill_parent" >
7      <com.your.test.CustomView android:layout_width="100px"
8                                android:layout_height="100px" />
9 </LinearLayout>
  最後是main activity檔案Test1.java: 1 package com.your.test;
2
3 public class Test extends Activity {
4     @Override
5     public void onCreate(Bundle savedInstanceState{
6         super.onCreate(savedInstanceState);
7         setContentView(R.layout.main);
8     }
9 }
把該import的import了,執行應該能看到一個正常的黑色背景的view。 下面應用我們定義的style試試看: 1 <com.your.test.CustomView android:layout_width="100px"
2                           android:layout_height="100px"
3                           style="@style/purple"
4 />
view的背景變成了紫色,但如果你log一下就會發現,呼叫的還是C2。 在AndroidManifest.xml裡用theme指定,結果也差不多(細節差別可自己體會,不贅述)。   下面我們就來研究defStyle到底接受什麼樣的引數。   首先把style和theme的引用都去掉,還原到黑色背景的view。這樣在程式裡R.style.purple就是這個style的顯式引用(其實到現在我也不知道doc裡說的explicit style resource是不是就是這個意思……)那麼,理論上我們把R.style.purple當作defStyle傳給C3,是不是就能做到設定view的預設背景為紫色呢? 1 public CustomView(Context context, AttributeSet attrs{
2     this(context, attrs, R.style.purple);
3 }
如果你log一下,就會發現,C2確實執行了,甚至R.style.purple也成功傳給C3裡的defStyle了,但是,view的背景還是黑色。   這是為什麼呢?是doc不對還是我不對?   這個先暫且放下不談,我們先試試那另外一種方式,傳入一個引用style資源的屬性(類似R.attr.buttonStyle)。這先要建立一個res/values/attrs.xml的檔案,這個檔案用來定義某個view裡可以出現的屬性: 1 <?xml version="1.0" encoding="utf-8"?>
2 <resources>
3     <declare-styleable name="CustomView">
4         <attr name="ourstyle" format="reference" />
5         <attr name="atext" format="string" />
6     </declare-styleable>
7 </resources>
現在我們為CustomView增加了兩個可以出現的自定義屬性,ourstyle和atext,前者就是我們打算用來引用一個style資源的屬性,後者是一個沒什麼用的字串屬性,放在這裡只是為了後面做測試。 現在我們就可以在程式裡引用這個屬性並把這個引數傳給defStyle。   當然,在這之前我們先要把purple這個style賦值給ourstyle。給一個view的屬性賦值,就和給android:layout_width賦值一樣,除了名稱空間不同(layout/main.xml的LinearLayout標籤裡有名稱空間的宣告): 1 <com.your.test.CustomView android:layout_width="100px"
2                           android:layout_height="100px"
3                           myxmlns:ourstyle="@style/purple"
4                           myxmlns:atext="test string"
5 />
也可以用指定theme的方法,在theme裡給所有的CustomView都賦予一個相同的預設的ourstyle值,然後應用這個theme: 在styles.xml裡另外定義一個style作為theme: 1 <style name="purpletheme">
2     <item name="ourstyle">@style/purple</item>
3 </style>
在AndroidManifest.xml的Application標籤中應用theme: 1 android:theme="style/purpletheme"
這兩種指定屬性的方法不同,在程式裡引用這個屬性的方法也不同。theme指定的屬性,可以直接用R.attr.ourstyle來引用,也可以用R.styleable.CustomView[R.styleable.CustomView_ourstyle]來引用,於是: 1 //C2
2 public CustomView(Context context, AttributeSet attrs{
3     this(context, attrs, R.attr.ourstyle );
4 }
這樣就成功地讓defStyle生效了。 那麼直接在標籤裡賦值的屬性怎麼引用呢? 直接在標籤裡賦值的屬性,都會在xml inflate時通過AttributeSet這個引數傳給C2,所以我們可以通過AttributeSet類提供的getAttributeResourceValue方法來獲取屬性的值。但是很可惜的是,我們只能獲取到屬性的值,而無法獲取包含這個值的屬性的引用(getAttributeNameResource方法返回的是和R.attr.ourstyle一樣的值,但這時R.attr.ourstyle並未指向@style/purple),這些亂七八糟的方法的各種值之間具體差別可以參考以下程式碼的log結果,相信仔細揣摩不難明白其中奧妙: 01 //C2
02 public CustomView(Context context, AttributeSet attrs{
03     this(context, attrs, attrs.getAttributeNameResource(2));
04     String a1 = ((Integer)R.attr.ourstyle).toString();
05     String a2 = ((Integer)R.styleable.CustomView_ourstyle).toString();
06     String a3 = ((Integer)R.styleable.CustomView[R.styleable.CustomView_ourstyle]).toString();
07     String a4 = ((Integer)R.style.purple).toString();
08     String a5 = ((Integer)attrs.getAttributeNameResource(2)).toString();
09     String a6 = ((Integer)attrs.getAttributeResourceValue(2,0)).toString();
10     String a7 = ((Integer)R.attr.atext).toString();
11     String a8 = ((Integer)R.styleable.CustomView[R.styleable.CustomView_atext]).toString();
12     String a9 = attrs.getAttributeValue(2);
13     String a10 = attrs.getAttributeValue(3);
14 }
log一下a1-10,示例結果如下: 01 a1 = 2130771968
02 a2 = 0
03 a3 = 2130771968
04 a4 = 2131034112
05 a5 = 2130771968
06 a6 = 2131034112
07 a7 = 2130771969
08 a8 = 2130771969
09 a9 = @2131034112
10 a10 = test string
凡是值相同的其實是一種意思,a1, a3, a5都指的是attrs.xml裡屬性的引用,這個引用id只有在theme裡賦值才有效,直接在標籤裡賦值是無效的。而傳這個id給defStyle就正符合doc裡寫的第一種情況。而a4, a6則直接代表了@style/purple的id,即doc裡寫的第二種情況: android <wbr>view建構函式研究

但是,回到我們最初的問題,傳R.style.purple,也就是2131034112這個id給defStyle是沒有效果的,為什麼呢?這個原因只能到android原始碼的view.java裡去看個究竟了: 1 public View(Context context, AttributeSet attrs, int defStyle{
2     this(context);
3     TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View,defStyle, 0);
4     [...other code...]
這就是view基類的建構函式C3,它在接受defStyle引數後利用context.obtainStyledAttributes這個方法來構造一個完整的屬性陣列,幾個引數綜合起來提供了從主題、樣式裡繼承來的和直接在標籤裡定義的所有屬性,具體可以看這個方法的doc:

obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)

Return a StyledAttributes holding the attribute values in set that are listed in attrs. In addition, if the given AttributeSet specifies a style class (through the "style" attribute), that style will be applied on top of the base attributes it defines.

Be sure to call StyledAttributes.recycle() when you are done with the array.

When determining the final value of a particular attribute, there are four inputs that come into play:

  1. Any attribute values in the given AttributeSet.
  2. The style resource specified in the AttributeSet (named "style").
  3. The default style specified by defStyleAttr and defStyleRes
  4. The base values in this theme.

Each of these inputs is considered in-order, with the first listed taking precedence over the following ones. In other words, if in the AttributeSet you have supplied <Button textColor="#ff000000">, then the button's text will always be black, regardless of what is specified in any of the styles.

Parameters
set The base set of attribute values. May be null.
attrs The desired attributes to be retrieved.
defStyleAttr An attribute in the current theme that contains a reference to a style resource that supplies defaults values for the StyledAttributes. Can be 0 to not look for defaults.
defStyleRes A resource identifier of a style resource that supplies default values for the StyledAttributes, used only if defStyleAttr is 0 or can not be found in the theme. Can be 0 to not look for defaults.
Returns
  • Returns a TypedArray holding an array of the attribute values. Be sure to call  when done with it.
於是我就納悶了,顯式的資源呼叫難道不是應該通過defStyleRes這個引數麼?為什麼這裡直接就寫成0了呢?這裡寫成0,那當然defStyle只能採取defStryleAttr的方式了。google了一下,還真在android的google code project裡發現了一個developer提交的Issue 12683提到了這種情況,不過也沒人comment,不知道究竟是不是這樣…… 文章出處:木人巷