android view构造函数研究

时间:2021-09-22 00:01:46
         上周遇到了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 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:

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

Since: API Level 1

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 TypedArray.recycle() when done with it.

于是我就纳闷了,显式的资源调用难道不是应该通过defStyleRes这个参数么?为什么这里直接就写成0了呢?这里写成0,那当然defStyle只能采取defStryleAttr的方式了。google了一下,还真在android的google code project里发现了一个developer提交的Issue 12683提到了这种情况,不过也没人comment,不知道究竟是不是这样……
文章出处:木人巷