自定义 View 之 自定义属性

创建一个自定义 View,最直接的方法是继承 View,然后重写其一系列方法,但有时候我们并不需要完全重新创建一个类,我们只需要简单的对现有组件做一下修改或者扩展,我们可以去继承现有的 View 类的子类。

假如我们需要拓展 TextView 类,我们需要给新的 TextView 添加干扰线,就好像验证码那样,我们先创建一个 AuthCode 类,让它继承 TextView:

public class AuthCode extends TextView{
    public AuthCode(Context context) {
        super(context);
    }

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

    public AuthCode(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public AuthCode(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}

在创建构造方法的时候,会提示 This custom view should extend android.support.v7.widget.AppCompatTextView instead,这是一个 warning 而不是 Error,可以不用理会。

可以看到,一共有四个构造方法,他们分别是:

  • 一个参数的:该方法主要用在代码中创建 AuthCode 对象时使用,Context 就是创建 AuthCode 的 Activity。
  • 两个参数的:多了一个 attrs 参数,该方法用于 XML 文件当中添加 AuthCode 组件,attrs 中保存着 AuthCode 的所有属性,例如 layout_widthlayout_heighttextmargin 等等。

SDK 目录platformsandroid-XXdataresvaluesattrs.xml 当中保存着所有组件支持的属性。例如 ImageView 的 src 属性定义如下:<attr name="src" format="reference|color" />

下面两个构造方法都和主题有关

  • 三个参数的:系统不会调用该方法,由 View 显示调用,其函数 defStyleAttr 为我们在清单文件中 <application> 或者 <activity> 通过 android:theme 指定的主题,在主题里有时候会指定一些控件的属性,例如 background 等等,当前两个构造函数没有为这些属性指定值时,会调用第三个参数中指定的。可以值可以为 0,表示不设置。
  • 四个参数的:第四个参数在第三个参数为 0 的时候或者在 theme 中找不到相关属性时使用,作为替补。

例如 Button 的构造函数:

    public Button(Context context) {
        this(context, null);
    }
    // 手动调用了第三个构造方法,并传入参数 com.android.internal.R.attr.buttonStyle
    public Button(Context context, AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.buttonStyle);
    }

    public Button(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

看完四个构造方法,我们开始自定义属性。

  • 步骤一:在 res/values 目录下添加 attrs.xml 文件,我们得属性就是创建在该文件下

attrs.xml 文件的根节点是 <resources>,然后每一组属性都是一个 <declare-styleable>,使用 name 作为标识,为做区分,也便于使用,通常我们使用类名为属性命名。

接下来就是创建属性值了,通过 <attr name="xxx" format="yyyy" /> 的格式创建,name 表示该属性的名称,例如 ImageView 的 src 等等,同样也要做到见名知意。format 为该属性的格式,有以下几种可选:

  • reference:引用资源
<!-- 属性定义 -->
<declare-styleable name = "名称">
     <attr name = "background" format = "reference" />
</declare-styleable>
<!-- 属性使用 -->
<ImageView android:background = "@drawable/image"/>
  • string:字符串
<!-- 属性定义 -->
<declare-styleable name = "名称">
     <attr name = "text" format = "string" />
</declare-styleable>
<!-- 属性使用 -->
<TextView android:text = "我是文本"/>
  • Color:颜色
<!-- 属性定义 -->
<declare-styleable name = "名称">
     <attr name = "textColor" format = "color" />
</declare-styleable>
<!-- 属性使用 -->
<TextView android:textColor = "#00FF00" />
  • boolean:布尔值
<!-- 属性定义 -->
<declare-styleable name = "名称">
     <attr name = "focusable" format = "boolean" />
</declare-styleable>
<!-- 属性使用 -->
<Button android:focusable = "true"/>
  • dimension:尺寸值
<!-- 属性定义 -->
<declare-styleable name = "名称">
     <attr name = "layout_width" format = "dimension" />
</declare-styleable>
<!-- 属性使用 -->
<Button android:layout_width = "42dip"/>
  • float:浮点型
<!-- 属性定义 -->
<declare-styleable name = "名称">
     <attr name = "fromAlpha" format = "float" />
</declare-styleable>
<!-- 属性使用 -->
<alpha android:fromAlpha = "1.0"/>
  • integer:整型
<!-- 属性定义 -->
<declare-styleable name = "名称">
     <attr name = "framesCount" format="integer" />
</declare-styleable>
<!-- 属性使用 -->
<animated-rotate android:framesCount = "12"/>
  • fraction:百分数
<!-- 属性定义 -->
<declare-styleable name = "名称">
     <attr name = "pivotX" format = "fraction" />
</declare-styleable>
<!-- 属性使用 -->
<rotate android:pivotX = "200%"/>
  • enum:枚举类型
<!-- 属性定义 -->
<declare-styleable name="名称">
    <attr name="orientation">
        <enum name="horizontal" value="0" />
        <enum name="vertical" value="1" />
    </attr>
</declare-styleable>
<!-- 属性使用 -->
<LinearLayout  
    android:orientation = "vertical">
</LinearLayout>
  • flag:位或运算
<!-- 属性定义 -->
<declare-styleable name="名称">
    <attr name="gravity">
            <flag name="top" value="0x30" />
            <flag name="bottom" value="0x50" />
            <flag name="left" value="0x03" />
            <flag name="right" value="0x05" />
            <flag name="center_vertical" value="0x10" />
    </attr>
</declare-styleable>
<!-- 属性使用 -->
<TextView android:gravity="bottom|left"/>

例如我们要给 TextView 添加干扰线,就可以定义一个属性值,用来表明干扰线的个数,这个属性就可以这样创建:

<resources>
    <declare-styleable name="AuthCode">
        <attr name="LineCount" format="integer" />
    </declare-styleable>
</resources>

然后是将属性应用起来,我们在写布局的时候需要先为该属性添加命名空间,命名空间的格式为 xmlns:空间名="http://schemas.android.com/apk/res-auto",例如:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:authcode="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    ...
</LinearLayout>

然后我们就可以在添加控件的时候为控件指定 authcode:LineCount 属性了:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:authcode="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.li_xyz.customview.AuthCode
        android:id="@+id/authcode"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="哈哈哈"
        authcode:LineCount="4" />
</LinearLayout>

设置了属性之后该如何获取呢?

Context 为我们提供了一个 obtainStyledAttributes 方法,该方法最终会返回一个包含由 sttrs.xml 中列出的属性的 TypedArray 对象。该对象是一个集合,通过调用 TypedArray 的 getXXX 方法可以获取我们在 attrs 当中定义的属性:

    private int lineCount;

    private void initParams(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.auth_code);
        if (typedArray != null) {
            lineCount = typedArray.getInteger(R.styleable.auth_code_LineCount, 2);
            typedArray.recycle();
        }
    }

为了方便我们在代码中方便的获取/设置属性值,往往还需要为属性添加 get/set 方法:

    @Override
    public int getLineCount() {
        return lineCount;
    }

    public void setLineCount(int lineCount) {
        this.lineCount = lineCount;
    }

这样,我们就可以在 Activity 当中操作该属性了,获取到了 LineCount,自然也就可以去绘制干扰线了(onDraw 方法中绘制,在后面会讲到)

TypedArray 对象的 getXXX 方法参数:

  • 第一个参数为要检索的属性索引,其格式为 R.styleable.属性集合名_属性名
  • 第二个参数为默认值,如果没有为该属性指定值,则默认为该指定值

Attributeset

我们看到,构造方法中有这个参数,那么它究竟是干什么的呢?

官方文档上讲,它是“与 XML 文件中的标签相关联的属性集合”,通常情况下,我们不直接使用该接口,而是将其传递给 Context 的 obtainStyledAttributes 方法去处理,该方法帮助我们去解析属性。

当然,我们也可以直接使用该接口:

    private void initParams(Context context, AttributeSet attrs) {
        int count = attrs.getAttributeCount();
        for (int i = 0; i < count; i++) {
            String name = attrs.getAttributeName(i);
            String value = attrs.getAttributeValue(i);
            Log.d("TTT", "name = " + name + ";value = " + value);
        }
    }

Log 如下:

08-10 22:51:04.349 15762-15762/com.li_xyz.customview D/TTT: name = id;value = @2131427422
08-10 22:51:04.351 15762-15762/com.li_xyz.customview D/TTT: name = layout_width;value = -2
08-10 22:51:04.352 15762-15762/com.li_xyz.customview D/TTT: name = layout_height;value = -2
08-10 22:51:04.352 15762-15762/com.li_xyz.customview D/TTT: name = text;value = 哈哈哈
08-10 22:51:04.353 15762-15762/com.li_xyz.customview D/TTT: name = LineCount;value = 4

可见, attrs 将 XML 当中所有属性都列出来啦~。