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布局你的的时候会被调用,
那么好像这两个方法就够了,为什么还有另外两个构造方法呢?我认为设计师应该是从代码的易用、健壮性等方面考虑的吧。后两个方法都是为了让我们可以在外部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
* 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.
*
When determining the final value of a particular attribute, there
* are four inputs that come into play:
*
*
*
Any attribute values in the given AttributeSet.*
The style resource specified in the AttributeSet (named* "style").
*
The default style specified by defStyleAttr and* defStyleRes
*
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
* textColor="#ff000000">
, then the button's text will
* always 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
这样会在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
@style/StyleForTheme
declare in base theme
declare in base theme
declare in base theme
declare in base theme
declare in base theme
declare in theme by style
declare in theme by style
declare in theme by style
declare in xml by style
declare in xml by style
declare in style for defStyleRes
declare in style for defStyleRes
declare in style for defStyleRes
declare in style for defStyleRes
先看AppTheme,里面就定义了一个名为StyleInTheme的item,他的属性是一个style中的引用。要明白,我们在attr.xml中可以不定义StyleInTheme这个attr,但如果这样做,那么在theme中设置StyleInTheme就变得没有意义,因为你无法在代码中获取theme的StyleInTheme属性。
activity_main.xml
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">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:mview_1="Direct declare in xml"
style="@style/MViewStyle"
/>
接下来是自定义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中设置了
@style/StyleForTheme
所以我们等会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的乱七八糟的关系后,我们的代码才能耦合性更低,更加健壮。