Android自定义View时, 自定义属性与构造方法defStyleAttr参数详解

/ Android其他 / 没有评论 / 297浏览

自定义属性常规套路

自定义View, 对Android开发者来说肯定不会陌生.通常在需要用到自定义属性时,我们都会在attrs.xml中按如下套路操作.

  1. 以自定义View的类名作为name,声明declare-styleable标签
   <declare-styleable name="CustomButton">
    
   </declare-styleable> 
  1. declare-styleable标签内,通过attr标签来定义属性的名称和类型.
   <declare-styleable name="CustomButton">
       <attr name="buttonType" format="enum">
           <enum name="type1" value="1"/>
           <enum name="type2" value="2"/>
           <enum name="type3" value="3"/>
       </attr>
       <attr name="buttonTextColor" format="color"/>
   </declare-styleable>
  1. 在自定义View的类中,至少声明1个和2个参数的构造方法.
public class CustomButton extends View {

   public CustomButton(Context context) {
       this(context, null);
   }

   public CustomButton(Context context, @Nullable AttributeSet attrs) {
       super(context, attrs);
   }
}
  1. 在构造方法中通过context.obtainStyledAttributes方法获取TypedArray,并最终通过TypedArray的各种getXXX方法取出自定义属性的值.
   public CustomButton(Context context, @Nullable AttributeSet attrs) {
       super(context, attrs);
       TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomButton, 0, 0);
       typedArray.getInt(R.styleable.CustomButton_buttonType, -1);
       typedArray.getColor(R.styleable.CustomButton_buttonTextColor, Color.RED);
       typedArray.recycle();
   }

不知道各位看官,尤其是刚入坑的小伙伴们有没有像曾经的我一样,有很多疑问. 为什么要按照这个套路来?declare-styleable标签中的name一定要是自定义View的类名吗? 一定要声明两个构造方法吗?obtainStyledAttributes方法几个参数分别是什么意思,等等..

别着急, 接下来就分享下我曾经的疑惑以及对应的理解.

Q&A

Q1

Q: declare-styleable标签中的name一定要与自定义View的类名相同吗?

A: 当然不一定,只是Google约定我们这样做,当不同时并不会影响我们的业务逻辑;我们尝试将declare-styleable中的name改为不同,Android StudioLint会有如下提示: alt 勉强过CET-4的渣渣翻译下: 根据约定,自定义Viewdeclare-styleable应该有相同的名称,(编辑器的各种功能依赖于此约定). 所以只是编辑器的各种功能依赖于此约定, 具体是什么哪些功能呢,最直接的就是没有代码提示了.

Q2

Q: declare-styleable标签是干嘛的,attr一定要声明在declare-styleable标签内吗?

A: declare-styleable表示一个包含了很多自定义属性的集合. 编译后会生成如下常量

        // R#attrs
        public static final int buttonType = 2130903145;
        public static final int buttonTextColor = 2130903142;

        // R#styleable
        public static final int[] CustomButton = new int[]{R.attr.buttonTextColor, R.attr.buttonType};
        public static final int CustomButton_buttonTextColor = 0;
        public static final int CustomButton_buttonType = 1;

我们在xml中定义的declare-styleableattr会在R的静态内部类styleableattr中根据name生成对应的常量. 而且还会额外生成自定义属性在数组中的索引. 那么attr标签一定要声明在descale-styleable中吗?答案也是否定的.

    <attr name="buttonType" format="enum">
        <enum name="type1" value="1"/>
        <enum name="type2" value="2"/>
        <enum name="type3" value="3"/>
    </attr>
    <attr name="buttonTextColor" format="color"/>
    <declare-styleable name="CustomButton">
        <attr name="buttonType"/>
        <attr name="buttonTextColor"/>
    </declare-styleable>

我们如上将attr定义在外部也同样没问题.而且当不同的declare-styleable中含有相同nameattr时,编译是无法通过(Error: Found item Attr/xxxx more than one time),这时,我们可以把attr定义在外部,就可以解决这个问题.

    <attr name="buttonType" format="enum">
        <enum name="type1" value="1"/>
        <enum name="type2" value="2"/>
        <enum name="type3" value="3"/>
    </attr>
    <attr name="buttonTextColor" format="color"/>
    <attr name="buttonTextSize" format="dimension"/>
    <declare-styleable name="CustomButton">
        <attr name="buttonType"/>
        <attr name="buttonTextColor"/>
    </declare-styleable>  
    <declare-styleable name="CustomLargeButton">
        <attr name="buttonType"/>
        <attr name="buttonTextColor"/>
    </declare-styleable>

Q3

Q: 一定要声明两个构造方法吗?

A: 实际上不是一定要声明参数为(Context context)(Context context, @Nullable AttributeSet attrs)的构造方法. 而是如果自定义View需要在xml中使用是,一定要直接或间接的调用参数为(Context context, @Nullable AttributeSet attrs)的构造方法.

    // 间接调用两个参数的构造方法
    public CustomButton(Context context) {
        super(context,null);
    }
   // 直接声明两个参数的构造方法
    public CustomButton(Context context) {
        super(context,null);
    }

    public CustomButton(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }   

否则在xml中使用时会报Caused by: java.lang.NoSuchMethodException: <init> [class android.content.Context, interface android.util.AttributeSet]的异常. 因为xml中的view最终会通过LayoutInflate类的createView(String name, String prefix, AttributeSet attrs)方法利用反射实例化,而调用就是签名为static final Class<?>[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class};的构造方法.

    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
 // name就是自定义view的类名
 clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);

                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
                // mConstructorSignature 参数签名是Context.class,AttributeSet.class
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
}

Q4

Q: 构造方法中的参数都是什么含义呢?

A: View构造方法参数最多有四个,签名是(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes), View的源码对四个参数分别做出了解释, 这里简单的翻译下

  1. context: View运行环境的上下文,可以通过context获取当前的theme和resource.
  2. attrs: View在xml中定义的属性集合, 可以通过attrs获取属性名和属性值
  3. defStyleAttr: 当前theme中的一个属性名称,它是一个引用并指向一个style,style中的属性会应用到当前view
  4. defStyleRes: 一个style类型的资源id,当defStyleAttr为0或者defStyleAttr不为0,但当前theme中不包含defStyleAttr对应的属性时, 会将defStyleRes中的属性应用到当前view

除了context,其他三个参数,文字描述可能比较抽象.下面分别演示下实例

attrs

    <cc.sadhu.neteasecloudmusic.view.CustomButton
            android:layout_width="200dp"
            app:buttonTextColor="@color/colorPrimary"
            app:buttonType="type1"
            android:padding="2dp"
            android:layout_height="200dp"/>

假如如上在xml中声明CustomButton, 在CustomButton的构造方法中,可以通过attrs.getAttributeName(),attrs.getAttributeValue()获取属性名称和值

            int attributeCount = attrs.getAttributeCount();
            for (int i = 0; i < attributeCount; i++) {
                String name = attrs.getAttributeName(i);
                String value = attrs.getAttributeValue(i);
                Log.d("CustomButton", "name:" + name + ";value:" + value);
            }
/*      D/CustomButton: name:padding;value:2.0dip
        D/CustomButton: name:layout_width;value:200.0dip
        D/CustomButton: name:layout_height;value:200.0dip
        D/CustomButton: name:buttonTextColor;value:@2131034159
        D/CustomButton: name:buttonType;value:1  */

defStyleAttr

View构造方法的第三个参数, 很多同学可能比较纳闷这个参数到底是干嘛的.

从上面的文字描述,我们知道他代表一个属性, 并且指向一个style.我们来看看到底怎么使用

  // 在attrs中定义一个reference类型的属性
    <attr name="customButtonStyle" format="reference"/>
  // 在styles中定义一个style,该style中有一个buttonType属性
    <style name="customStyle">
        <item name="buttonType">type2</item>
    </style>
  // 在我们的主题使用customButtonStyle属性.指向名为customStyle的style
   <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="customButtonStyle">@style/customStyle</item>
    </style>

    // 第三个参数就可以传入R.attr.customButtonStyle这个属性了
    public CustomButton(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, R.attr.customButtonStyle);
    }

我们可以从Android已有的View入手,来看看TextView,ButtondefStyleAttr传的是什么值.

    public TextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.textViewStyle);
    }
 
    public Button(Context context, AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.buttonStyle);
    }

从源码可以看出,他们的defStyleAttr分别是com.android.internal.R.attr.textViewStylecom.android.internal.R.attr.buttonStyle, 他们也是一个reference类型的属性,并且 系统的默认主题包含也两个属性.


        // attrs.xml
        <!-- Default TextView style. -->
        <attr name="textViewStyle" format="reference" />
        <!-- Normal Button style. -->
        <attr name="buttonStyle" format="reference" />

    // theme.xml中定义了很多默认属性
    <style name="Theme">
        <item name="textViewStyle">@style/Widget.TextView</item>  
        <item name="buttonStyle">@style/Widget.Button</item> 
    </style>
    // style.xml
    <style name="Widget.TextView">
        <item name="textAppearance">?attr/textAppearanceSmall</item>
        <item name="textSelectHandleLeft">?attr/textSelectHandleLeft</item>
        <item name="textSelectHandleRight">?attr/textSelectHandleRight</item>
        <item name="textSelectHandle">?attr/textSelectHandle</item>
        <item name="textEditPasteWindowLayout">?attr/textEditPasteWindowLayout</item>
        <item name="textEditNoPasteWindowLayout">?attr/textEditNoPasteWindowLayout</item>
        <item name="textEditSidePasteWindowLayout">?attr/textEditSidePasteWindowLayout</item>
        <item name="textEditSideNoPasteWindowLayout">?attr/textEditSideNoPasteWindowLayout</item>
        <item name="textEditSuggestionItemLayout">?attr/textEditSuggestionItemLayout</item>
        <item name="textEditSuggestionContainerLayout">?attr/textEditSuggestionContainerLayout</item>
        <item name="textEditSuggestionHighlightStyle">?attr/textEditSuggestionHighlightStyle</item>
        <item name="textCursorDrawable">?attr/textCursorDrawable</item>
        <item name="breakStrategy">high_quality</item>
        <item name="hyphenationFrequency">normal</item>
    </style>
    <style name="Widget.Button">
        <item name="background">@drawable/btn_default</item>
        <item name="focusable">true</item>
        <item name="clickable">true</item>
        <item name="textAppearance">?attr/textAppearanceSmallInverse</item>
        <item name="textColor">@color/primary_text_light</item>
        <item name="gravity">center_vertical|center_horizontal</item>
    </style>    

这样结合文字和代码应该就不难理解defStyleAttr是什么东西, 具体是怎么使用,我们待会再来分析.

defStyleRes

这个属性很少使用,它表示一个style,我们可以在xml中定义, 然后作为构造方法的第四个参数传入,具体是怎么使用,我们待会再来分析. .

    <style name="defaultStyle">
        <item name="buttonType">type3</item>
    </style>
    public CustomButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, R.style.defaultStyle);
    }

obtainStyledAttributes

上面的Q&A中已经介绍过了View构造方法四个参数的含义。我们也知道了参数中defStyleAttrdefStyleRes代表什么东西,但是我们还不知道这些参数有什么用.

其实他们都是用于给View设置属性,这些属性的值就通过context.obtainStyledAttributes()方法获取出来.该方法有四个参数,具体使用方式如下:

public CustomButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomButton, defStyleAttr, defStyleRes);
        typedArray.recycle();
    }

可以看到,除了第二个参数,其他都来自构造方法的传参.不需要过多介绍.第二个参数传入了一个我们之前声明的declare-styleable.该方法返回类型是TypedArray.

总结来说, 一个view包含的属性.声明在declare-styleable中,这些属性可以在很多地方复制,比如在布局文件:attrs,在当前主题中:defStyleAttr, obtainStyledAttributes就是将这些这些属性对应的值从可能被赋值的地方取出来,并将相关信息放到TypedArray中, 而且为了扩展性, 还给我们一个兜底的方案: 当布局文件没有声明或者主题中没有声明.我们还可以从defStyleRes取出默认值.

文字描述大家可能还是觉得很抽象, 我们依然用例子来说明.

我们先在attrs中定义两个自定义属性buttonTextColorbuttonType,通过declare-styleable声明它们在CustomButton这个View中使用.同时定义个自定义属性customButtonStyle,类型是reference,表示customButtonStyle指向一个style.

    <attr name="buttonTextColor" format="color"/>
    <attr name="buttonType" format="enum">
        <enum name="type1" value="1"/>
        <enum name="type2" value="2"/>
        <enum name="type3" value="3"/>
        <enum name="type4" value="4"/>
        <enum name="type5" value="5"/>      
    </attr>
    
    <attr name="customButtonStyle" format="reference"/>

    <declare-styleable name="CustomButton">
        <attr name="buttonType"/>
        <attr name="buttonTextColor"/>
    </declare-styleable>

我们想给CustomButtonbuttonType属性赋值有多少种方式呢

  1. 在布局文件中直接设置buttonType属性
  2. 在布局文件中设置style, 在style中设置buttonType属性
  3. 在布局文件中设置theme, 在theme指向的style中设置customButtonStyle, 在customButtonStyle指向的style中设置buttonType属性
  4. AndroidManifest设置当前布局文件对应的Activitytheme,在该theme指向的style中设置customButtonStyle,在customButtonStyle指向的style中设置buttonType属性
  5. 声明一个style,在该style中设置buttonType属性,并将该参数作为obtainStyledAttributes方法的第四个参数传入.

有这么多地方可以设置属性值, 肯定也有个优先级吧. 其实上面的顺序就是对应的优先级.

我们验证下, 上代码:

  1. attrs.xml
   <attr name="buttonTextColor" format="color"/>
   <attr name="buttonType" format="enum">
       <enum name="type1" value="1"/>
       <enum name="type2" value="2"/>
       <enum name="type3" value="3"/>
       <enum name="type4" value="4"/>
       <enum name="type5" value="5"/>
   </attr>

   <attr name="customButtonStyle" format="reference"/>

   <declare-styleable name="CustomButton">
       <attr name="buttonType"/>
       <attr name="buttonTextColor"/>
   </declare-styleable>
  1. styles.xml
   <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
       <!-- Customize your theme here. -->
       <item name="colorPrimary">@color/colorPrimary</item>
       <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
       <item name="colorAccent">@color/colorAccent</item>
       <item name="customButtonStyle">@style/customStyle</item>
   </style>

   <style name="customStyle">
       <item name="buttonType">type4</item>
       <item name="buttonTextColor">@color/colorPrimary</item>
   </style>

   <style name="defaultStyle">
       <item name="buttonType">type5</item>
   </style>

   <style name="layoutTheme">
       <item name="customButtonStyle">@style/styleInLayoutTheme</item>
   </style>
   <style name="layoutStyle">
       <item name="buttonType">type2</item>
   </style>
   <style name="styleInLayoutTheme">
       <item name="buttonType">type3</item>
   </style>
  1. AndroidManifest.xml
       <activity android:name=".ui.activity.RankListActivity" android:theme="@style/AppTheme"/>

  1. 布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:app="http://schemas.android.com/apk/res-auto"
             android:orientation="vertical"
             android:id="@+id/llContent"
             android:layout_width="match_parent"
             android:layout_height="match_parent">

   <cc.sadhu.neteasecloudmusic.view.CustomButton
           android:layout_width="200dp"
           app:buttonTextColor="@color/colorPrimary"
           app:buttonType="type1"
           style="@style/layoutStyle"
           android:theme="@style/layoutTheme"
           android:padding="2dp"
           android:layout_height="200dp"/>
</LinearLayout>

  1. CusttomButton.java
public class CustomButton extends View {
   public CustomButton(Context context) {
       super(context, null);
   }

   public CustomButton(Context context, @Nullable AttributeSet attrs) {
       this(context, attrs, R.attr.customButtonStyle);
   }

   public CustomButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
       this(context, attrs, defStyleAttr, R.style.defaultStyle);
   }

   public CustomButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
       super(context, attrs, defStyleAttr, defStyleRes);
       TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomButton, defStyleAttr, defStyleRes);
       int type = typedArray.getInt(R.styleable.CustomButton_buttonType, -1);
       Log.d("CustomButton", "type:" + type);
       typedArray.recycle();
   }
}

我们直接运行,打印结果如下: D/CustomButton: type:1 说明上述五种声明方式中,第一种,在布局文件中直接设置buttonType属性优先级最高.

我们将布局文件夹中的app:buttonType="type1"删掉, 打印结果如下: D/CustomButton: type:2 说明第二种方式,style中的优先级次之.

我们继续将布局文件中的style="@style/layoutStyle"删掉,打印结果如下: D/CustomButton: type:3 说明第三种方式,theme中的优先级再次之.

我们接着将布局文件中的android:theme="@style/layoutTheme"删掉,打印结果如下: D/CustomButton: type:4 说明第三种方式,theme中的优先级再次之.

最后我们把AppTheme主题中的customButtonStyle属性删掉, 打印如下结果: D/CustomButton: type:5 这说明defStyleRes作为兜底的方案生效了.

以上也验证了我们的优先级结论.

总结

一不小心, 就写了万把来字, 虽然不是很复杂的问题, 结论也很简单. 但是带着问题去思考的话,会让我们理解得更深入.而且在看源码的过程中也会将很多知识点串联起来. 比如布局文件中的view如何实例化;LayoutInflate是如何解析布局文件的;设置属性优先级等问题.有机会,我们再来吹吹逼~~