Android中Attributes、defStyleAttr、

2017-08-01  本文已影响370人  Alien水哥

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的乱七八糟的关系后,我们的代码才能耦合性更低,更加健壮。

参考文章:Android中自定义样式与View的构造函数中的第三个参数defStyle的意义

上一篇下一篇

猜你喜欢

热点阅读