Android中Attributes、defStyleAttr、
Android开发中必不可少的是自定义控件,关于这个网上有一大堆文章教你写自定义控件,但我想很多人在写自定义控件的自定义属性的时候,肯定对一些style,attrs感到一头雾水或不能完全明白,以前自己也是。下面就来梳理下加深理解。
首先自定义控件一般有以下四个构造方法:
public class MView extends View{
public MView(Context context) ;
public MView(Context context, @Nullable AttributeSet attrs);
public MView(Context context, @Nullable AttributeSet attrs, int defStyleAttr);
public MView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) ;
}
你真的明白这四个构造方法的意义么?
这里先说明下,如果你继承一些兼容库的控件比如AppCompatTextView,他是不提供四个参数的构造方法的。
我们要搞明白,这些参数的意义,其实它们的作用无非就是两种:
1、我要取什么属性;
2、我从哪里取我需要的属性;
1)先看第一个构造方法,他只有一个context,这个构造方法使用在代码中直接new出一个控件,它不附带任何自定义属性。
2)第二个构造方法,当你在xml布局你的<MView/>的时候会被调用,
那么好像这两个方法就够了,为什么还有另外两个构造方法呢?我认为设计师应该是从代码的易用、健壮性等方面考虑的吧。后两个方法都是为了让我们可以在外部style中直接给我们的自定义控件设置属性。
我们先来看看源码中的构造方法都做了什么:
View.java
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
......
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.View_background:
background = a.getDrawable(attr);
break;
......
我们可以看到View的四参构造方法中,它调用了obtainStyledAttributes方法,获取到TypedArray,再从TypedArray中获取相应的属性,比如background。这里没贴出来的是,View的其它三个构造方法最终都调用了这个四参构造方法。为了更好的理解,我们进入到obtainStyledAttributes方法中看看。context的这个方法,最终是调用了Resource.obtainStyledAttributes,他的注释里有以下一段
Resource.java
/**
* Return a TypedArray holding the attribute values in
* <var>set</var>
* that are listed in <var>attrs</var>. 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.
* <p>When determining the final value of a particular attribute, there
* are four inputs that come into play:</p>
*
* <ol>
* <li> Any attribute values in the given AttributeSet.
* <li> The style resource specified in the AttributeSet (named
* "style").
* <li> The default style specified by <var>defStyleAttr</var> and
* <var>defStyleRes</var>
* <li> The base values in this theme.
* </ol>
*
* <p>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 <code><Button
* textColor="#ff000000"></code>, then the button's text will
* <em>always</em> be black, regardless of what is specified in any of
* the styles.
*/
public TypedArray obtainStyledAttributes(AttributeSet set,
@StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
return mThemeImpl.obtainStyledAttributes(this, set, attrs, defStyleAttr, defStyleRes);
}
注释里写的很清楚了,这个方法就是拿来取设置的属性的。但是这个取有一个顺序,在注释的中间部分,按以下优先级:
1、任何在AttributeSet中给出的;
2、在AttributeSet中的style属性中设置的;
3、从defStyleAttr和defStyleRes中设置的;
4、在Theme中直接设置的属性。
任何的说明都不如一个实例来的直白,下面我们就结合代码来讲讲这四个顺序是啥意思,defStyleAttr和defStyleRes到底是啥。
首先是attr.xml
<resources>
<!-- 这个应该很常见不用解释 -->
<declare-styleable name="MView">
<attr name="mview_1" format="string"/>
<attr name="mview_2" format="string"/>
<attr name="mview_3" format="string"/>
<attr name="mview_4" format="string"/>
<attr name="mview_5" format="string"/>
</declare-styleable>
<!-- 这个属性下面有解释-->
<attr name="StyleInTheme" format="reference"/>
</resources>
这样会在R类中这个生成以下代码:
public static final class attr {
public static final int StyleInTheme=0x7f010000;
public static final int mview_1=0x7f0100d9;
public static final int mview_2=0x7f0100da;
......
}
public static final class styleable {
public static final int[] MView = {
0x7f0100d9, 0x7f0100da, 0x7f0100db, 0x7f0100dc,
0x7f0100dd
};
public static final int MView_mview_1 = 0;
public static final int MView_mview_2 = 1;
......
}
不难理解,每个我们声明的attr都在R.attr下生成了一份,顺便生成了个R.styleable.MView,与值从0~4对应的MView_mview_1。
先做个说明,StyleInTheme就是为了让我们能在Activity的Theme中使用我们自定义的属性而定义的,这也对应着我们的defStyleAttr,接着看。
以下是style.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="StyleInTheme">@style/StyleForTheme</item>
<item name="mview_1">declare in base theme</item>
<item name="mview_2">declare in base theme</item>
<item name="mview_3">declare in base theme</item>
<item name="mview_4">declare in base theme</item>
<item name="mview_5">declare in base theme</item>
</style>
<style name="StyleForTheme">
<item name="mview_1">declare in theme by style</item>
<item name="mview_2">declare in theme by style</item>
<item name="mview_3">declare in theme by style</item>
</style>
<style name="MViewStyle">
<item name="mview_1">declare in xml by style</item>
<item name="mview_2">declare in xml by style</item>
</style>
<style name="StyleForDefStyleRes">
<item name="mview_1">declare in style for defStyleRes</item>
<item name="mview_2">declare in style for defStyleRes</item>
<item name="mview_3">declare in style for defStyleRes</item>
<item name="mview_4">declare in style for defStyleRes</item>
</style>
</resources>
先看AppTheme,里面就定义了一个名为StyleInTheme的item,他的属性是一个style中的引用。要明白,我们在attr.xml中可以不定义StyleInTheme这个attr,但如果这样做,那么在theme中设置StyleInTheme就变得没有意义,因为你无法在代码中获取theme的StyleInTheme属性。
activity_main.xml
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.testattr.MainActivity">
<com.example.testattr.MView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:mview_1="Direct declare in xml"
style="@style/MViewStyle"
/>
</android.support.constraint.ConstraintLayout>
接下来是自定义view:MView.java
public class MView extends View {
public MView(Context context) {
this(context,null);
}
public MView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,R.attr.StyleInTheme);
}
public MView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr,R.style.StyleForDefStyleRes);
}
public MView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.MView,defStyleAttr,defStyleRes);
Log.e("...","mview_1: " + a.getString(R.styleable.MView_mview_1));
Log.e("...","mview_2: " + a.getString(R.styleable.MView_mview_2));
Log.e("...","mview_3: " + a.getString(R.styleable.MView_mview_3));
Log.e("...","mview_4: " + a.getString(R.styleable.MView_mview_4));
Log.e("...","mview_5: " + a.getString(R.styleable.MView_mview_5));
a.recycle();
}
}
context.obtainStyledAttributes方法接受四个参数,其实父类的构造方法中也是用这个方法去取属性的。
先对四个参数的构造方法的参数做个归类:
Attributes、defAttr、defStyle都属于告诉程序从哪去属性。
第二个参数接收int[] 类型,这里我们传入了R.styleable.MView,这里面包含了mview_1~mview_5这些属性名。这里你完全可以使用
new int[]{R.attr.mview_1,...}这种方式来传参,这也是一种应用方式。
R.styleable.MView就是告诉程序需要取这个int数组中定义的这些属性。
Attributes参数中包含的属性是从MView的xml布局中获取的,比如上面的acitivity_main.xml代码,他能获取到mview_1:"Direct declare in xml".优先级对应注释中提到的四个优先级的第一个:
Any attribute values in the given AttributeSet.
其次,我们再MView布局中设置了一个style="@style/MViewStyle",那么我们代码中能获取到MViewStyle中设置的属性:mview_2,至于mview_1也在这里面定义了?不存在的,因为MView控件里的直接设置属性优先级要比MView控件style中设置的属性优先级高,view的style中的属性属于第二优先级。对应于:
The style resource specified in the AttributeSet (named "style").
再看MView的第二个构造方法中,调用了第三个构造方法,上面说过了,我们定义R.attr.StyleInTheme就是为了在这里把它当成defStyleAttr使用的,这个参数是可以为0的,那就是不设置默认style嘛。我们先看TextView源码:
public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.textViewStyle);
}
TextView也是传入了一个attr,这个defStyleAttr参数是告诉我们去哪去默认值——Theme.你完全可以给你的activity的Theme设置android:textViewStyle,那么这个style适用于你App中所有的TextView。
回看style.xml,我们就在AppTheme中设置了
<item name="StyleInTheme">@style/StyleForTheme</item>
所以我们等会Log可以看到,TypeArray中取到的mview_3="declare in theme by style",
再然后看我们吧R.style.StyleForDefStyleRes作为defStyleRes传入,defStyleAttr应该跟defStyleRes一起讲,因为它们就像obtain方法中讲的那样,同属于第三优先级,只不过defStyleRes跟defStyleAttr比起来要靠后,放在上面代码里:
当defStyleAttr不为0,且对应的Theme中设置了相关style(指的是StyleInTheme)的时候,即defStyleAttr生效,那么defStyleRes不生效。
当defStyleAttr=0,或者对应Theme没有设置该style,那么defStyleRes生效。
以上是根据代码运行结果得出的结论。
最后就是The base values in this theme。指的是再Theme中直接设置的属性,对应于mview_5。
运行日志:
E: mview_1: Direct declare in xml
E: mview_2: declare in xml by style
E: mview_3: declare in theme by style
E: mview_4: declare in base theme
E: mview_5: declare in base theme
可以看到mview_4使用的是Theme中直接定义的属性,与mview_5一致。
我们修改MView的第二个构造方法,不传入defStyleAttr:
public MView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
修改后运行日志:
E: mview_1: Direct declare in xml
E: mview_2: declare in xml by style
E: mview_3: declare in style for defStyleRes
E: mview_4: declare in style for defStyleRes
E: mview_5: declare in base theme
这样mview_3 view_4就使用了defStyleRes中设置的属性了。
明确理解defStyleAttr、defStyleRes与什么Theme、declare-style、attr的乱七八糟的关系后,我们的代码才能耦合性更低,更加健壮。