目录
前面一篇博文,主要介绍的Android自定义View的几种方式,那么定义的这个控件,肯定有一些属性希望在布局文件中可以直接进行配置。像在使用Android的系统控件的时候,都是以android:开头的一些属性,可以在布局文件中简单的配置就去设置某些属性值,在Android源码中系统的values文件夹维护一个attrs.xml文件来定义了这些属性,例如经常使用的layout_width:
<declare-styleable name="ViewGroup_Layout">
<!-- Specifies the basic width of the view. This is a required attribute
for any view inside of a containing layout manager. Its value may
be a dimension (such as "12dip") for a constant width or one of
the special constants. -->
<attr name="layout_width" format="dimension">
<!-- The view should be as big as its parent (minus padding).
This constant is deprecated starting from API Level 8 and
is replaced by {@code match_parent}. -->
<enum name="fill_parent" value="-1" />
<!-- The view should be as big as its parent (minus padding).
Introduced in API Level 8. -->
<enum name="match_parent" value="-1" />
<!-- The view should be only big enough to enclose its content (plus padding). -->
<enum name="wrap_content" value="-2" />
</attr>
........
</declare-styleable>
所以在自定义View的时候,同样也需要自定义属性,可以在布局文件或者其他地方中进行配置。
一 自定义属性
1.在attrs.xml中声明属性
从上面提到的layout_width源码中,可以看到,在定义一个属性的时候,需要通过<attr name="xxx" format="xxx" /> 这种标签的形式在attrs.xml声明属性。通常有两种方式:
一种就是通过<declare-styleable>标签进行声明,那么这种方式声明的属性,系统会生成一个属性的数组 R.styleable.GridView,每一个属性的索引就是R.styleable.GridView_name,通过该索引找到对应的属性。
<resources>
<declare-styleable name="GridView">
<attr name="verticalSpacing" format="dimension" />
<attr name="horizontalSpacing" format="dimension" />
<attr name="numColumns" format="integer" />
<!-- 初始化的个数-->
<attr name="initNum" format="integer" />
<!--测试不同的构造函数调用周期-->
<attr name="name" format="string" />
</declare-styleable>
</resources>
另外一种就是可以直接通过 <attr>进行声明,那么这种方式声明的属性就是一个元素,直接通过 R.attr.GridViewStyle的方式找到对应的属性值
<resources>
<!--通过单独的属性来设置属性值-->
<attr name="GridViewStyle" format="reference" />
</resources>
参数说明:
参数 | 备注 | 举例 | |
name | 很简单就是这个属性的名字 | ||
format | reference | 该属性可以设置资源文件的ID | <attr name="textAppearance" format="reference" /> |
color | 该属性为颜色值 | <attr name="titleTextColor" format="color" /> | |
boolean | 该属性可以设置为对应数据类型的数值 | <attr name="hint" format="string" /> | |
float |
|||
integer | |||
string | |||
dimension | 该属性可以设置为尺寸值,可以是具体带单位的尺寸值,也可以为R.dimen的引用 |
<attr name="layout_margin" format="dimension" /> | |
enum |
该属性可以设置为枚举值,使用的时候只能使用一种枚举值 | <attr name="visibility"> |
|
flag | 该属性可以设置为位或运算 | <attr name="textStyle"> <flag name="normal" value="0" /> <flag name="bold" value="1" /> <flag name="italic" value="2" /> </attr> |
|
fraction | 该属性可以设置为百分比的数值 | <attr name="centerX" format="float|fraction" /> | |
混合 | 当时属性指定的时候也可以使用多种类型值,可以通过|指定 | <attr name="gradientRadius" format="float|fraction|dimension" /> |
2.在View的构造函数中获取属性值
通常在自定义View的构造函数中通过context.obtainStyledAttribute获取到布局文件中设置的属性。
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.GridView, defStyleAttr, defStyleRes)
主要将布局文件里面定义的在R.styleable.GridView下定义的属性的值读出来。后面会具体说明下属性值在不同的地方赋值的优先级。那么在attrs.xml中定义的属性值,就可以通过类似下面的方式读出赋值。
mVerticalSpacing = array.getDimensionPixelSize(R.styleable.GridView_verticalSpacing, 0);
mHorizontalSpacing = array.getDimensionPixelOffset(R.styleable.GridView_horizontalSpacing, 0);
maxNumber = array.getInt(R.styleable.GridView_maxNumber, 0);
mNumColumns = array.getInt(R.styleable.GridView_numColumns, 4);
initNum = array.getInt(R.styleable.GridView_initNum, 1);
name = array.getString(R.styleable.GridView_name);
3.在布局文件中使用
<com.android.attrsetting.grid.GridView
android:id="@+id/gv_test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="50px"
psv:horizontalSpacing="50px"
psv:initNum="1"
psv:name="直接定义在布局文件中 设置的名字 优先级为1"
psv:verticalSpacing="50px" />
另外要在布局文件中增加新的命名才可以使用这些自定义的属性。
xmlns:psv="http://schemas.android.com/apk/res-auto"
二 属性赋值的优先级
在第一部分定义的各个属性,可以在多个地方赋值:像“布局xml赋值”、“布局xml中引用style”、“由defStyleAttr指定的style”、“由defStyleRes指定的style”、“theme中直接赋值”。在自定义View中通过context.obtainStyledAttributes(attrs, R.styleable.GridView, defStyleAttr, defStyleRes)取值的时候,那么到底以哪个地方的赋值为准呢?
通过几个例子来说明下不同设置值的优先级。
1.自定义控件GridView中增加属性name
<!--测试不同的构造函数调用周期-->
<attr name="name" format="string" />
2.在GridView的初始化构造函数,通过下面的代码获取到name的值
private void initAttributes(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
if (attrs == null) {
return;
}
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.GridView, defStyleAttr, defStyleRes);
if (array == null) {
return;
}
name = array.getString(R.styleable.GridView_name);
........
}
其中 public final TypedArray obtainStyledAttributes(@Nullable AttributeSet set, @NonNull @StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)方法可以获取在R.styleable.GridView中定义的各种属性集合。
set:就是该控件定义在布局文件中设置的各种属性,包括系统属性和自定义属性
attrs:需要获取的属性的数组。也就是我们需要获取到布局文件中哪些属性。通常为我们自定义的R.styleable.xxx(如果xxx在定义的时候使用<declare-styleable>声明,则返回的就是一个数组集合,可参见“1.在attrs.xml中声明属性”的介绍)。
剩下的两个参数可与Android自定义View的四种方式中提到的构造函数的四个参数的后两个参数。
3.在不同的地方设置属性值,验证优先级
第一个实例:GridViewAttrTheme1SettingActivity
- (1)将GridViewAttrTheme1SettingActivity注册在AndroidManifest的主题为:
<activity
android:name="com.android.attrsetting.GridViewAttrTheme1SettingActivity"
android:theme="@style/GridViewTheme" />
该 @style/GridViewTheme代码如下:
<!--在Activity定义Theme时使用defStyleAttr-->
<style name="GridViewTheme">
<item name="initNum">5</item>
<!--Theme中直接赋值-->
<item name="name">通过在Theme的添加name属性 设置的名字 优先级5</item>
<item name="GridViewStyle">@style/GridViewStyleInTheme</item>
</style>
<style name="GridViewStyleInTheme">
<item name="initNum">3</item>
<!--在Theme中指定defStyleAttr-->
<item name="name">通过Theme的defStyleAttr属性 设置的名字 优先级3</item>
</style>
即在Themem中直接给name属性赋值,也在Theme指定defStyleAttr。该defStyleAttr定义在GridView中的代码如下:
public GridView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.GridViewStyle, 0);
}
- (2)在布局文件中添加下面几个GridView的控件
1)第一个GridView控件:在布局文件中设置name的属性值
<!--优先级为1的情况:在布局文件中定义name属性-->
<com.android.attrsetting.grid.GridView
android:id="@+id/gv_test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="50px"
psv:horizontalSpacing="50px"
psv:initNum="1"
psv:name="直接定义在布局文件中 设置的名字 优先级为1"
psv:verticalSpacing="50px" />
2)第二个GridView控件:在布局文件中引用style指向该name属性
<!--优先级为2的情况:在布局文件的style属性中定义name属性-->
<com.android.attrsetting.grid.GridView
android:id="@+id/gv_test111"
style="@style/GridViewInLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="50px"
psv:horizontalSpacing="50px"
psv:initNum="2"
psv:verticalSpacing="50px" />
对应的@style/GridViewInLayout的代码如下:
<style name="GridViewInLayout">
<item name="initNum">2</item>
<item name="name">通过布局文件引入style,在该style中 设置的名字 优先级为2</item>
</style>
3)第三个GridView控件:不在布局文件中设置name属性和style引用指向name属性,仅在Activity的Theme指定defStyleAttr。
<!--优先级为3的情况:在Theme中通过defStyleAttr属性定义name属性,在控件中使用android:theme不起作用-->
<com.android.attrsetting.grid.GridView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="50px"
android:theme="@style/GridViewThemeAttr0"
psv:horizontalSpacing="50px"
psv:verticalSpacing="50px" />
<!--优先级为3的情况:在Theme中通过defStyleAttr属性定义name属性-->
<com.android.attrsetting.grid.GridView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="50px"
psv:horizontalSpacing="50px"
psv:verticalSpacing="50px" />
这两个GridView控件的区别还在于验证在控件中设置android:theme将不起作用。
- (3)在GridView控件给defStyleRes指定的style
if (defStyleRes <= 0) {
defStyleRes = R.style.DefaultGridViewStyleRes;
}
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.GridView, defStyleAttr, defStyleRes);
if (array == null) {
return;
}
name = array.getString(R.styleable.GridView_name);
其中 R.style.DefaultGridViewStyleRes的代码如下:
<style name="DefaultGridViewStyleRes">
<item name="name">自定义UI的默认的styleRes样式中 设置的名字 优先级4</item>
<item name="initNum">4</item>
</style>
那么对于第一个GridView控件来说,上述的5种设置属性的方式都存在;第二个GridView控件来说,除去在布局文件中直接对name赋值之外,其他的四种赋值方式都存在;第三个和第四个GridView控件来说,只存在“由defStyleAttr指定的style”、“由defStyleRes指定的style”、“theme中直接赋值”三种赋值方式,那么看下在初始化GridViewAttrTheme1SettingActivity的时候,每个GridView的name属性值为:
2020-11-30 14:45:07.874 25978-25978/com.android.widgetplaceholder V/GridView.initAttributes(L:129): name = 直接定义在布局文件中 设置的名字 优先级为1
2020-11-30 14:45:07.876 25978-25978/com.android.widgetplaceholder V/GridView.initAttributes(L:129): name = 通过布局文件引入style,在该style中 设置的名字 优先级为2
2020-11-30 14:45:07.878 25978-25978/com.android.widgetplaceholder V/GridView.initAttributes(L:129): name = 通过Theme的defStyleAttr属性 设置的名字 优先级3
2020-11-30 14:45:07.880 25978-25978/com.android.widgetplaceholder V/GridView.initAttributes(L:129): name = 通过Theme的defStyleAttr属性 设置的名字 优先级3
从日志中可以看到:“布局xml赋值” > “布局xml中引用style” > “由defStyleAttr指定的style”。因为只有依次去掉优先级高的设置name属性的方式,才可以得到优先级低点的设置name属性方式的值。在看下另外另种方式的优先级。
第二个实例:GridViewAttrTheme2SettingActivity
- (1)将GridViewAttrTheme2SettingActivity注册在AndroidManifest的主题为:
<activity
android:name="com.android.attrsetting.GridViewAttrTheme2SettingActivity"
android:theme="@style/GridViewThemeAttr0" />
其中@style/GridViewThemeAttr0的代码如下:
<!--在Activity定义Theme时无默认的style应用-->
<style name="GridViewThemeAttr0">
<item name="initNum">5</item>
<item name="name">通过在Theme的添加name属性并且没有defStyleAttr 设置的名字 优先级为5</item>
</style>
该主题中仅有在Theme中直接给name属性赋值。
- (2)在布局文件中添加GridView控件
<!--优先级为4/5的情况:在Theme中不定义defStyleAttr属性定义name属性,默认的会使用在控件中定义的defStyleRes中属性-->
<com.android.attrsetting.grid.GridView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="50px"
psv:horizontalSpacing="50px"
psv:verticalSpacing="50px" />
那么对于该控件来说,仅有 “由defStyleRes指定的style”、“theme中直接赋值”两种设置name属性的方式。初始化GridViewAttrTheme2SettingActivity,看下打印出来的日志:
2020-11-30 15:03:05.950 25978-25978/com.android.widgetplaceholder V/GridView.initAttributes(L:129): name = 自定义UI的默认的styleRes样式中 设置的名字 优先级4
从日志中可以看到 “由defStyleRes指定的style”>“theme中直接赋值”,并且只有在没有设置第一个实例中的三种设置name属性方式的情况下,“由defStyleRes指定的style”才会起作用。
第三个实例: GridViewAttrTheme3SettingActivity
(1)将GridViewAttrTheme3SettingActivity注册在AndroidManifest的主题为:
<activity
android:name="com.android.attrsetting.GridViewAttrTheme3SettingActivity"
android:theme="@style/GridViewThemeAttr1" />
其中@style/GridViewThemeAttr1的代码如下:
<!--验证优先级5-->
<style name="GridViewThemeAttr1">
<item name="initNum">5</item>
<item name="name">通过在Theme的添加name属性并且没有defStyleAttr 设置的名字 优先级为5</item>
<item name="GridViewStyle">@style/GridViewStyleNoInTheme</item>
</style>
<style name="GridViewStyleNoInTheme">
<item name="initNum">5</item>
</style>
可以看到该Theme中对name直接赋值,同时也设置了由defStyleAttr指定的style,但是style中并没有name属性。
(2)在布局文件中添加GridView控件
<!--优先级为4/5的情况:在Theme中不定义defStyleAttr属性定义name属性,默认的会使用在控件中定义的defStyleRes中属性-->
<com.android.attrsetting.grid.GridView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="50px"
psv:horizontalSpacing="50px"
psv:verticalSpacing="50px" />
初始化GridViewAttrTheme2SettingActivity,看下打印出来的日志:
2020-11-30 15:03:32.184 25978-25978/com.android.widgetplaceholder V/GridView.initAttributes(L:129): name = 通过在Theme的添加name属性并且没有defStyleAttr 设置的名字 优先级为5
从打印出来的日志可以看出,如果在自定义控件的时候,如果设置了 defStyleRes,那么要让Theme中设置的name属性起作用,必须在Theme中设置defStyleAttr指定的style,但是style并不对name属性赋值。
三 总结
这种自定义属性的方式可以方便的将控件的属性通过像布局文件、style等方式进行赋值,并且可以随着主题的不同来设置不同的值。那么设置属性的优先级为“布局xml赋值”>“布局xml中引用style”>“由defStyleAttr指定的style”>“由defStyleRes指定的style”>“theme中直接赋值”。另外如果设置了defStyleRes,那么只有在Theme中设置name的属性起作用,必须在Theme中设置defStyleAttr指向style,并且该style中不对属性进行赋值。
另外在本文中涉及的代码已经上传github。相关代码主要为com/android/attrsetting相关代码。