Android 从 0 开始自定义控件之自定义属性详解(十一)

3,436 阅读11分钟

转载请标明出处: blog.csdn.net/airsaid/art…
本文出自:周游的博客

前言

和自定义 View 打交道,肯定是难免要写自定义属性的。虽然我们可以直接使用 Android 本身一些系统控件定义的属性,但是在实际开发中,由于我们所自定义 View 的多样性,所以我们就需要自己来定义属于我们所编写自定义控件的属性了。

定义

定义自定义属性非常简单,只需在 res/values/ 下新建 attrs.xml 文件(默认新建项目没有这个文件),并在文件中编写如下实例代码:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="custom_color" format="color"/>

    <declare-styleable name="CustomAttribute">
        <attr name="custom_radius" format="dimension"/>
        <attr name="custom_color"/>
    </declare-styleable>
</resources>

其中 attr 和 declare-styleable 节点分别代表的意思如下:

  • attr: 定义了一个属性,属性名为 custom_color 这个是可以随意起的,但是要注意不要和其他控件所冲突, format 所定义的是属性的格式,其中格式又分为好多种,下面会细说,这里定义的是颜色 color。

  • declare-styleable:定义了一个属性组,在里面我们可以单独写 attr 属性,也可以引用直接在 resources 下定义的 attr,其中的区别就是引用的不用写 format。

需要注意的是,attr 并不依赖与 declare-styleable,declare-styleable 只是方便了 attr 的使用,使属性的使用更加明确。两者在代码中的获取方式并不相同,下面会细说。

在实际开发中,我们一般是采用 declare-styleable 方式,直接定义一组自己所编写的自定义控件需要用到的属性。

格式

上面说到了 format 指定自定义属性的格式,那么到底一共有多少种格式呢?答案是十种,分别如下:

  • reference:参考某一资源 ID。

定义:

<declare-styleable name="名称">
    <attr name="src" format="reference"/>
</declare-styleable>

使用:

app:src="@drawable/图片ID"
  • color:颜色值。

定义:

<declare-styleable name="名称">
    <attr name="color" format="color"/>
</declare-styleable>

使用:

app:color="#FFFFFF"
app:color="@color/颜色ID"
  • boolean:布尔值。

定义:

<declare-styleable name="名称">
    <attr name="click" format="boolean"/>
</declare-styleable>

使用:

app:clickable="true"
  • dimension:尺寸值。

定义:

<declare-styleable name="名称">
    <attr name="layout_width" format="dimension"/>
</declare-styleable>

使用:

app:layout_width="50dp"
  • float:浮点值。

定义:

<declare-styleable name="名称">
    <attr name="radius" format="float"/>
</declare-styleable>

使用:

app:radius="10.5"
  • integer:整型值。

定义:

<declare-styleable name="名称">
    <attr name="duration" format="integer"/>
</declare-styleable>

使用:

app:duration="1000"
  • fraction:百分数。

定义:

<declare-styleable name="名称">
    <attr name="pivotX" format="fraction"/>
</declare-styleable>

使用:

app:pivotX="50%"
  • string:字符串。

定义:

<declare-styleable name="名称">
    <attr name="text" format="string"/>
</declare-styleable>

使用:

app:text="Hello World!"
  • enum:枚举值。

定义:

<declare-styleable name="名称">
    <attr name="orientation">
        <enum name="horizontal" value="0"/>
        <enum name="vertical" value="1"/>
    </attr>
</declare-styleable>

使用:

app:orientation="horizontal"
app:orientation="vertical"
  • flag:位或运算。

定义:

<declare-styleable name="名称">
    <attr name="gravity">
        <flag name="top" value="0"/>
        <flag name="center" value="1"/>
        <flag name="bottom" value="2"/>
    </attr>
</declare-styleable>

使用:

app:gravity="center|bottom"

注意事项:

属性定义时也可以指定多种类型值,比如:

<declare-styleable name="名称">
    <attr name="background" format="reference|color"/>
</declare-styleable>

使用:

app:background="@drawable/图片ID|#FFFFFF"

获取

上面知道了如何定义自定义属性,现在该了解下如何获取了!获取的话,可以分为两种,一种是直接获取 resources 节点下定义的 attr,还有就是 declare-styleable,首先来看下第一种是如何获取的吧。

首先我们来定义好自定义属性:

<resources>
    <attr name="custom_color" format="color"/>
</resources>

当我们每定义一个 attr 时就会在 R 文件中生成一个对应的 ID,所以我们想获取 attr 的话,就可以根据这个 ID 来获取:

public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    int[] customAttrs = {R.attr.custom_color};
    TypedArray a = context.obtainStyledAttributes(attrs, customAttrs);
    int color = a.getColor(0, Color.WHITE);
    a.recycle();
}

Android 系统中其实也定义了很多的属性,就在sdk\platforms\android-23\data\res\values目录下,我们同样也可以获取系统中原本定义的属性:

int[] customAttrs = {android.R.attr.color};
TypedArray a = context.obtainStyledAttributes(attrs, customAttrs);
mColor = a.getColor(0, mColor);
a.recycle();

如果你要是觉得上面这种获取方法太麻烦了,懒得写 int 数组的话,就来试试获取 declare-styleable 吧!
依然首先来定义好属性:

<declare-styleable name="CustomAttribute">
    <attr name="custom_color" format="color"/>
    <attr name="custom_radius" format="dimension"/>
</declare-styleable>

当我们定义 declare-styleable 时,R 文件里就自动给我们生成了一个 int[],其中的 attr 就是每一个元素,所以获取时,就不用再单独定义 int[] 了:

public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomAttribute);
    mColor = a.getColor(R.styleable.CustomAttribute_custom_color, mColor);
    mRadius = a.getDimension(R.styleable.CustomAttribute_custom_radius, mRadius);
    a.recycle();
}

嗯,得确方便了那么一丢丢。使用这种方式,我们也可以获取系统中的属性,只需把系统的属性引用过来:

<declare-styleable name="CustomAttribute">
    <attr name="custom_color" format="color"/>
    <attr name="custom_radius" format="dimension"/>
    <attr name="android:color"/>
</declare-styleable>

获取:

 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomAttribute);
//        mColor = a.getColor(R.styleable.CustomAttribute_custom_color, mColor);
 mColor = a.getColor(R.styleable.CustomAttribute_android_color, mColor);
 mRadius = a.getDimension(R.styleable.CustomAttribute_custom_radius, mRadius);
 a.recycle();

有实践的朋友,可能会发现 obtainStyledAttributes 方法可是有好几种重载方法呢!虽然上面这种能用,但是不知道其他几种的意思,心里感觉总是少点什么,不得劲,所以还是赶紧来了解下剩下几种重载方法的意思吧!

obtainStyledAttributes

obtainStyledAttributes 方法是 context 的,该方法一共有四个重载方法,分别如下

  • obtainStyledAttributes(int[] attrs):从系统主题中获取 attrs 中的属性。
  • obtainStyledAttributes(int resid, int[] attrs):从资源文件中获取 attrs 中的属性。
  • obtainStyledAttributes(AttributeSet set, int[] attrs):从 layout 设置的属性中获取 attrs 中的属性。
  • obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes):下面细说。

猛一看,很蒙B,都是啥啊?具体我们先不用管,只需在脑海中有个概念:”我们需要获取哪些属性,从哪里获取“。

obtainStyledAttributes(int[] attrs)

首先来看下第一个方法,该方法是从系统主题中,也就是 theme 中获取属性,那么首先就需要在 manifest 指定好 theme:

android:theme="@style/AppTheme"

接下来在 attrs 文件中定义好,我们需要获取的属性:

<declare-styleable name="CustomAttribute">
    <attr name="custom_color" format="color"/>
    <attr name="custom_radius" format="dimension"/>
</declare-styleable>

需要获取的属性定义好了,那么从哪里获取呢? 当时是我们刚开始指定的 theme 了,在 styles 文件中指定好数据源:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!--直接在主题中指定-->
    <item name="custom_color">#FF0000</item>
    <item name="custom_radius">100dp</item>
</style>

在代码中获取:

TypedArray a = context.getTheme().obtainStyledAttributes(R.styleable.CustomAttribute);
mColor = a.getColor(R.styleable.CustomAttribute_custom_color, mColor);
mRadius = a.getDimension(R.styleable.CustomAttribute_custom_radius, mRadius);
a.recycle();

obtainStyledAttributes(int resid, int[] attrs)

该方法是从资源文件中获取 attrs 中的属性,首先依然还是需要在 attrs 文件中定义好我们需要获取的属性:

<declare-styleable name="CustomAttribute">
    <attr name="custom_color" format="color"/>
    <attr name="custom_radius" format="dimension"/>
</declare-styleable>

知道要获取哪些了,那么从哪里获取呢? 在 styles 文件指定好数据源:

<style name="CustomTheme">
    <item name="custom_color">#00FF00</item>
    <item name="custom_radius">10dp</item>
</style>

在代码中获取:

TypedArray a = context.getTheme().obtainStyledAttributes(R.style.CustomTheme, R.styleable.CustomAttribute);
mColor = a.getColor(R.styleable.CustomAttribute_custom_color, mColor);
mRadius = a.getDimension(R.styleable.CustomAttribute_custom_radius, mRadius);
a.recycle();

obtainStyledAttributes(AttributeSet set, int[] attrs)

该方法从 layout 设置的属性中获取 attrs 中的属性。我们在 layout 中设置的属性,如:

<com.github.airsaid.customattributedemo.widget.CircleView
    app:custom_color="#0000FF"
    style="@style/CircleView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

最终都会在 AttributeSet 中,View 会在构造中传递过来,需要注意的是,AttributeSet 也同时包含了 setyle 中设置的属性。

这也就说明了,为什么我们在自定义 View 的时候,都要重写:public CircleView(Context context, AttributeSet attrs)构造参数。因为如果不重写的话,我们将无法获取到在 layout 中设置的属性。

接下来还是通过该方法来获取我们定义的属性,首先依然是定义好需要需要获取的属性:

<declare-styleable name="CustomAttribute">
    <attr name="custom_color" format="color"/>
    <attr name="custom_radius" format="dimension"/>
</declare-styleable>

这时的数据源,就需要在 layout 布局中设置了:

<com.github.airsaid.customattributedemo.widget.CircleView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:custom_color="#0000FF"
    app:custom_radius="50dp"/>

在代码中获取:

TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomAttribute);
mColor = a.getColor(R.styleable.CustomAttribute_custom_color, mColor);
mRadius = a.getDimension(R.styleable.CustomAttribute_custom_radius, mRadius);
a.recycle();

obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)

该方法是最后的一个方法,上面的三个重载方法,其实最终都是走了该方法。

下面来演示下用该方法的获取属性,首先依然是定义我们需要获取的属性:

<declare-styleable name="CustomAttribute">
    <attr name="custom_type" format="string"/>

    <attr name="customStyle" format="reference"/>
</declare-styleable>

XML 布局中(这里指定的是 set 参数中要获取的属性):

<com.github.airsaid.customattributedemo.widget.CircleView
    app:custom_type="1:在 XML 布局中声明属性"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

Styles 文件中(这里指定的是 defStyleAttr 参数用到的属性):

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="customStyle">@style/DefaultCustomStyle</item>
</style>

<style name="DefaultCustomStyle">
    <item name="custom_type">2:主题中声明的默认样式属性</item>
</style>

Styles 文件中(这里指定的是 defStyleRes 参数要用到的属性):

<style name="MyCustomStyle">
    <item name="custom_type">3:默认资源样式属性</item>
</style>

注意,要在 manifest 文件中指定当前 theme:

android:theme="@style/AppTheme"

在代码中获取:

TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomAttribute, R.attr.customStyle, R.style.MyCustomStyle);
String type = a.getString(R.styleable.CustomAttribute_custom_type);
Log.e("test", "type: " + type);
a.recycle();

打印结果:

01-15 11:04:10.008 7921-7921/com.github.airsaid.customattributedemo E/test: type: 1:在 XML 布局中声明属性

通过结果可以看出,因为我们已经在 XML 中声明好属性了,此时的 attrs 参数中是有值的,所以就会优先获取 attrs 中的属性值,这时,如果我们把 attrs 改为 null 试试:

TypedArray a = context.obtainStyledAttributes(null, R.styleable.CustomAttribute, R.attr.customStyle, R.style.MyCustomStyle);

重新运行,打印结果:

01-15 11:05:58.377 7921-7921/com.github.airsaid.customattributedemo E/test: type: 2:主题中声明的默认样式属性

通过上面的打印结果可以发现,当 attrs 是 null 时,会在当前主题中寻找默认声明的属性值。那么如果我们此时把该参数改为 0 呢? 试试看:

TypedArray a = context.obtainStyledAttributes(null, R.styleable.CustomAttribute, 0, R.style.MyCustomStyle);

重新运行,打印结果:

01-15 11:09:25.142 7921-7921/com.github.airsaid.customattributedemo E/test: type: 3:默认资源样式属性

果不其然,当 defStyleAttr 参数为 0 或者无法在对应的主题下找到资源文件时,那么会再去寻找 defStyleRes 中指定的资源文件中声明的属性。如果此时再把 defStyleRes 也改为 0 的话,那么不用说,获取时肯定是 null 的,因为已经没有数据源可以获取了。

实例

自定义属性已经了解的差不多了,接下来该写一个完整的自定义 View 练练手了,这里我们把上篇中 自定义 View 基础实例 中的第一个实例给搬过来吧,毕竟还说过要完善它的!

  • 第一步:定义自定义 View 需要的属性。
<declare-styleable name="CustomAttribute">
    <attr name="custom_color" format="color"/>
    <attr name="custom_radius" format="dimension"/>
</declare-styleable>
  • 第二步:编写自定义 View。
public class CircleView extends View {

    private Paint mPaint;
    private int mColor = Color.WHITE;

    /** 圆半径 */
    private float mRadius = 0;

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

    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomAttribute);
        mColor = a.getColor(R.styleable.CustomAttribute_custom_color, mColor);
        mRadius = a.getDimension(R.styleable.CustomAttribute_custom_radius, mRadius);
        a.recycle();
        init();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if(widthMode == MeasureSpec.AT_MOST){
            widthSize = (int) (mRadius * 2 + getPaddingLeft() + getPaddingRight());
        }

        if(heightMode == MeasureSpec.AT_MOST){
            heightSize = (int) (mRadius * 2 + getPaddingTop() + getPaddingBottom());
        }

        setMeasuredDimension(widthSize, heightSize);
    }

    private void init() {
        // 初始化画笔
        mPaint = new Paint();
        mPaint.setColor(mColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();

        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        canvas.drawCircle(width / 2 + paddingLeft, height / 2 + paddingTop,  mRadius, mPaint);
    }
}
  • 第三步:在 XML 布局中声明自定义 View:
<com.github.airsaid.customattributedemo.widget.CircleView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#999999"
    app:custom_color="#FF0000"
    app:custom_radius="20dp"/>

注意事项:

别忘记,加上命令空间:

xmlns:app="http://schemas.android.com/apk/res-auto"

运行结果:

这里写图片描述