Android 中 View 的构造函数浅解

构造函数的调用时机

在自定义 View 的过程当中,不可避免的需要接触到 View 的构造函数,目前 View 具有四个构造函数,分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public View(Context context) {
/...../
}

public View(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
/...../
}
  • defStyleAttr 当前主题中的一个属性,其中包含对为视图提供默认值的样式资源的引用。 可以为0,不寻找默认值。
  • defStyleRes 为视图提供默认值的样式资源的资源标识符,仅在 defStyleAttr 为0或在主题中找不到时使用。可以为0,不寻找默认值。

第一个构造函数源码的解释如下: > Simple constructor to use when creating a view from code.

也就是说当在代码中创建 View 的时候会调用第一个构造函数。

第二个构造函数的源码解释如下: > Constructor that is called when inflating a view from XML. This is called when a view is being constructed from an XML file, supplying attributes that were specified in the XML file. This version uses a default style of 0, so the only attribute values applied are those in the Context's Theme and the given AttributeSet.

也就是说当在 XML 文件中插入 View 的时候会调用第二个构造函数,并且会应用 Context 的主题以及在 XML 中给定的属性值。

第三个和第四个构造函数的源码解释分别如下: > Perform inflation from XML and apply a class-specific base style from a theme attribute. > > >This constructor of View allows subclasses to use their own base style when they are inflating.

For example, a Button class's constructor would call this version of the super class constructor and supply R.attr.buttonStyle for defStyleAttr; this (in particular its background) as well as the Button class's attributes.


Perform inflation from XML and apply a class-specific base style from a theme attribute or style resource.

This constructor of View allows subclasses to use their own base style when they are inflating.

也就是说两个构造函数共同的作用是允许 View 应用自己的基础 style,那么当我们需要为 View 设置 style 的时候就可以选择去调用这两个构造函数当中的一个,调用方式一般如下:

1
2
3
4
5
6
7
public Button(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.buttonStyle);
}

public Button(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}

defStyleAttr 和 defStyleRes 的联系

接下来的问题是这两者的区别在哪里?由于这两者都用于设置 style,而 style 里面是属性的集合,所以这里需要先了解 View 设置属性的方式,一般来说有如下五种方式: 1. XML 中直接声明相关属性值 2. XML 中 引入 style 3. defStyleAttr 4. defStyleRes 5. theme中直接指定

既然有五种方式,那么当同时应用的时候就涉及到了优先级的问题。为了了解优先级的问题,首先我们需要在 attrs.xml 文件中写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DefAttrs">
<attr name="attr_1" format="string" />
<attr name="attr_2" format="string" />
<attr name="attr_3" format="string" />
<attr name="attr_4" format="string" />
</declare-styleable>

<attr name="defStyleAttr" format="reference" />
</resources>
  • 这里定义了四个格式为 string 的属性用于测试,定义了一个名为 defStyleAttr 引用。
  • 通过 declare-styleable 可以为每个属性在 R 文件里自动生成一个 int[],这样就可以很方便的用 R.styleable.* 来进行使用,当然定义的属性也可以不放在 declare-styleable 中,但是使用的时候就需要通过如下代码:

    1
    2
    int[] attrs = {R.attr.attr_1,R.attr_2};
    TypedArray typedArray = context.obtainStyledAttributes(set,attrs);

在创建了自定义的属性之后,还需要创建一些 style,在 style.xml 文件中写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<resources>

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>

<!--Theme 直接指定-->
<item name="attr_1">theme_one</item>
<item name="attr_2">theme_two</item>
<item name="attr_3">theme_three</item>
<item name="attr_4">theme_four</item>
<!--defStyleAttr-->
<item name="defStyleAttr">@style/def_style_attr</item>
</style>

<!--xml 中引入的 style -->
<style name="xml_style">
<item name="attr_1">xml_style_one</item>
<item name="attr_2">xml_style_two</item>
</style>

<!--defStyleAttr 引用的 style-->
<style name="def_style_attr">
<item name="attr_1">def_style_one</item>
<item name="attr_2">def_style_two</item>
<item name="attr_3">def_style_three</item>
</style>

<!--defStyleRes-->
<style name="def_style_res">
<item name="attr_1">def_style_res_one</item>
<item name="attr_2">def_style_res_two</item>
<item name="attr_3">def_style_res_three</item>
<item name="attr_4">def_style_res_four</item>
</style>
</resources>

上述代码添加了四种 style,对应为 View 设置属性的方式中的后四种,在添加了 style 之后,我们还需要应用 style,所以需要先创建一个如下的自定义 View:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class MyView extends TextView{

public MyView(Context context) {
super(context);
}

public MyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.defStyleAttr);
}

public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr,R.attr.defStyleAttr);
parse(context, attrs, defStyleAttr);
}

@TargetApi(VERSION_CODES.LOLLIPOP)
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

private void parse(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DefAttrs, defStyleAttr, R.style.def_style_res);
String one = typedArray.getString(R.styleable.DefAttrs_attr_1);
String two = typedArray.getString(R.styleable.DefAttrs_attr_2);
String three = typedArray.getString(R.styleable.DefAttrs_attr_3);
String four = typedArray.getString(R.styleable.DefAttrs_attr_4);

log("one = " + one);
log("two = " + two);
log("three = " + three);
log("four = " + four);

typedArray.recycle();
}

private void log(String msg) {
Log.i("MyView", msg);
}
}
  • 第二个构造函数中显式调用了第三个构造函数,并设置 defStyleAttr 参数值为 R.attr.defStyleAttr。
  • parse() 获取四个属性值并进行打印。

创建完自定义 View 之后,现在需要在 XML 文件中添加自定义的View:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:test="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.rookieyang.test.MainActivity">

<com.rookieyang.test.MyView
test:attr_1="xml_attr_attr1"
style="@style/xml_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />


</android.support.constraint.ConstraintLayout>
  • test:attr_1="xml_attr_attr1" XML 中直接指定 attr_1 属性值。
  • style="@style/xml_style" XML 中引入 style。

运行结果

在完成上述步骤之后就可以运行,运行之后的输出如下: 运行结果一 上述代码中,XML 中直接声明 attr_1 属性值为 xml_attr_attr1,XML 中 引入的 style 中给 attr_1,attr_2 赋值为 xml_style_one,xml_style_two,defStyleAttr 给 attr_1,attr_2,attr_3 赋值为 def_style_one,def_style_two,def_style_three,defStyleRes 和 Theme 也分别为四个属性进行了赋值,而根据输出的结果在优先级上我们很容易得到以下结论: > XML 中直接声明相关属性值 > XML 中 引入 style > defStyleAttr > theme中直接指定 > > defStyleAttr > defStyleRes

从第一部分可以知道由于 defStyleAttr 的存在,defStyleRes 没有生效,所以 defStyleRes 和 theme中直接指定的优先级还不能进行判断,这个时候需要将 style.xml 文件中的下列代码进行注释:

1
<item name="defStyleAttr">@style/def_style_attr</item>

或者将自定义 View 中调用第三个构造函数的代码做如下修改:

1
2
//将第三个参数更改为 0
this(context, attrs, defStyleAttr,0);

修改之后运行能得到如下输出: 运行结果二 可以看到当 defStyleAttr 不存在后 defStyleRes 开始生效,并且优先级要高于theme中直接指定。 所以最终结论如下: > XML 中直接声明相关属性值 > XML 中 引入 style > defStyleAttr > defStyleRes > theme中直接指定 > 仅在 defStyleAttr 为0或在主题中找不到时,defStyleRes 生效。


参考资料

  1. 深度解析View构造函数中的参数defStyleAttr
  2. Android View 四个构造函数详解
  3. 深入理解Android 自定义attr Style styleable以及其应用