美文网首页
Android中Attributes、defStyleAttr、

Android中Attributes、defStyleAttr、

作者: Alien水哥 | 来源:发表于2017-08-01 15:24 被阅读370次

    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的意义

    相关文章

      网友评论

          本文标题:Android中Attributes、defStyleAttr、

          本文链接:https://www.haomeiwen.com/subject/figdlxtx.html