27
2017
09

Android自定义View-自定义属性

介绍

自定义View的自定义属性,为了能让自定义View在xml文件中编写时可以设置自己特有的属性。用代码写界面不需要自定义属性。

声明自定义属性

创建attrs.xml文件

在res/values/中创建一个attrs.xml。

创建declare-styleable节点

在根节点resources内添加declare-styleable节点,declare-styleable节点只有一个name属性且为必填,name可以自由定义。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomView">
    </declare-styleable>
</resources>

declare-styleable节点会在R.styleable中生成常量名为name的值的int数组常量,用于后面获取自定义属性时从属性集合里获取出只在declare-styleable节点里的attr的TypedArray。

public static final int[] CustomView = {
};

创建attr节点(声明自定义属性)

attr节点有两个属性name和format,其中name为必填可以自由定义,format用于规定自定义属性的值的类型,有10种类型。
format的8种类型和对应的声明和赋值例子:

format类型 声明例子 赋值例子
boolean <attr name="focusable" format="boolean" /> <View android:focusable="true" />
integer <attr name="progress" format="integer" /> <ProgressBar android:progress="100" />
float <attr name="alpha" format="float" /> <View android:alpha="0.6" />
dimension(dp、px等) <attr name="padding" format="dimension" /> <View android:padding="30dp" />
string <attr name="hint" format="string" /> <TextView android:hint="asdf" />
fraction(nn%p) <attr name="customFraction" format="fraction"></attr> <ojin.customview.CustomView app:customFraction="20%p" />
reference(@) <attr name="id" format="reference" /> <RelativeLayout android:id="@+id/activity_main" />
color(#) <attr name="textColor" format="reference|color" /> <TextView android:textColor="#444444" />

format可以同时定义成多个类型,用 | 分隔。像上面的color。

还有两种类型,enum(枚举)和flag(位或)。
attr节点有两种子节点,enum和flag,enum和flag不能同时使用。
enum可在format声明,也可以不声明,当attr节点内有enum节点时,attr可以赋值为enum。
flag不可在format声明,当attr节点内有flag节点时,attr可以赋值为flag。

enum子节点的声明,value的值只能是int类型

<attr name="layout_width" format="dimension">
    <enum name="fill_parent" value="-1" />
    <enum name="match_parent" value="-1" />
    <enum name="wrap_content" value="-2" />
</attr>

enum子节点使用:只能使用其中一个值

<view android:layout_width="wrap_content" />
<view android:layout_width="match_parent" />
<view android:layout_width="30dp" />

flag子节点的声明,value的值只能是int类型

<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" />
    <flag name="fill_vertical" value="0x70" />
    <flag name="center_horizontal" value="0x01" />
    <flag name="fill_horizontal" value="0x07" />
    <flag name="center" value="0x11" />
    <flag name="fill" value="0x77" />
    <flag name="clip_vertical" value="0x80" />
    <flag name="clip_horizontal" value="0x08" />
    <flag name="start" value="0x00800003" />
    <flag name="end" value="0x00800005" />
</attr>

flag子节点使用:可以单个赋值,或用 | 分隔赋多个值

<view android:gravity="left" />
<view android:gravity="left|top"" />

直接在根节点resources内创建

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="customFraction" format="fraction"></attr><!--resources节点中创建attr节点-->
</resources>

在declare-styleable节点内添加attr节点

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomView">
        <attr name="customFraction" format="dimension"></attr><!--declare-styleable节点中创建attr节点-->
    </declare-styleable>
</resources>

attr节点的两种创建方式都会在R.attr中生成常量名为name的值的int常量,所以attr节点是唯一的(就算format不同),用name区别。

public static final int customFraction=0x7f010097;

两种创建attr节点的区别

声明在declare-styleable节点中的attr节点,appt会做两件事情:
1、在declare-styleable节点对应的R.styleable中的常量数组增加一个值,这个值是attr节点在R.attr中对应的值。

public static final int[] CustomView = {
    0x7f010097
};

2、在R.styleable中生成对应常量名为declare-styleable节点name_attr节点name的整形常量,这个常量的值是一个角标,对应上面CustomView常量数组中该attr节点的值的角标,用于方便从TypedArray中获取自定义属性的值。

public static final int CustomView_customFraction = 0;//declare-styleable节点name_attr节点name

这两件事情都是为了方便在获取自定义属性的值。
声明在根节点resources内的attr节点什么也没做。

引用attr节点

直接引用android系统自带的attr

name前面加上android:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomView">
        <attr name="android:orientation" /><!--引用android系统自带的attr-->
    </declare-styleable>
</resources>

跟使用android系统自带的attr一样

<ojin.customview.CustomView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"/>

引用自己声明的attr

name直接写要引用的attr的name的值,这里先声明两个自定义属性customFraction1和customFraction2。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="customFraction1" format="fraction" /><!--声明在根节点resources内-->

    <declare-styleable name="CustomView2">
        <attr name="customFraction2" format="fraction" /><!--声明在declare-styleable节点内-->
    </declare-styleable>

    <declare-styleable name="CustomView1">
        <attr name="customFraction1" /><!--引用根节点resources的attr-->
        <attr name="customFraction2" /><!--引用其它declare-styleable节点的attr-->
    </declare-styleable>
</resources>

跟使用直接创建的attr一样

<ojin.customview.CustomView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:customFraction1="1%"
    app:customFraction2="2%" />

引用的attr不要设置format或者添加子节点,这样会认为这是一个新的attr,但是同样name的attr只能声明一次,所以会编译不通过。

使用自定义属性

声明命名空间

xmlns:asdfasdf="http://schemas.android.com/apk/res-auto"//asdfasdf可以自由定义

设置自定义属性

<ojin.customview.CustomView  android:layout_width="match_parent" android:layout_height="match_parent" asdfasdf:customFraction2="2%" /><!--asdfasdf:开头-->

获取自定义属性的值

获取TypedArray

通过Context类的两个方法从AttributeSet中获取所需属性的TypedArray,用完记得回收。

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

参数:

  • set:属性值的基本集
  • attrs:需要检索的所需属性
  • defStyleAttr:当前主题中的一个属性,它包含一个样式资源的引用,该资源为TypedArray提供默认值。可以是0,不查找默认值。
  • defStyleRes:为TypedArray提供默认值的样式资源的资源标识符,仅在defStyleAttr为0或不能在主题中找到时使用。可以是0,不查找默认值。
public CustomView(Context context, AttributeSet attrs) {
    super(context, attrs);
    //获得typedArray
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView);

    typedArray.recycle();//回收typedArray
}

构造函数里面的attrs是在xml里面声明的属性的集合,包括layout_width、layout_height等父类的属性。
要从这个集合里取出相关的属性,就要通过上面Context类的两个方法中第二个参数传入一个数组,这个数组代表需要检索的属性,上面代码传入了R.styleable.CustomView,R.styleable.CustomView就是上面两种创建attr节点的区别提到的int数组常量,这样属性集合中关于CustomView的属性就可以从返回的typedArray中获取。

可以自己组建一个数组来替代R.styleable.CustomView,这样可以从typedArray中获取不是声明在declare-styleable节点内的attr节点。

private final int[] mAttrs = new int[]{R.attr.customFraction, android.R.attr.layout_width};

推荐用这种方式进行获取属性值:

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //获得typedArray
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView);

        final int N = typedArray.getIndexCount();
        for (int i = 0; i < N; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.CustomView_customFraction:
                    typedArray.getFraction(attr, 1, 1, 0);
                    //对获取到的自定义属性进行处理
                    break;
            }
        }

        typedArray.recycle();//回收typedArray
    }

为什么不直接从构造函数里的attrs中直接获取属性值?

attrs中包含的是所有赋了值的属性,而且不知道里面的顺序、没有getIndex(int),只能遍历判断attributeName进行获取。

public CustomView(Context context, AttributeSet attrs) {
    super(context, attrs);

    final int N = attrs.getAttributeCount();
    for (int i = 0; i < N; i++) {
        switch (attrs.getAttributeName(i)) {
            case "layout_width":
                String layout_width = attrs.getAttributeValue(i);
                break;
        }
    }
}

一些format对应类型的获取方法:
index从0到attrs.length-1,对应获取TypedArray时传入的第二个参数数组的角标
defValue是默认值,没有设置该属性时返回默认值

format类型 获取方法 说明
boolean boolean getBoolean(int index, boolean defValue)
integer int getInt(int index, int defValue
int getInteger(int index, int defValue) 如果format是string,值是123,会抛异常。getInt(int,int)不会。
float float getFloat(int index, float defValue)
dimension float getDimension(int index, float defValue) 返回值是像素
int getDimensionPixelOffset(int index, int defValue) 对getDimension(int,float)的返回值取整,float强转成int
int getDimensionPixelSize(int index, int defValue 对getDimension(int,float)的返回值取整,如果float强转成int不为0,四舍五入,否则(如果float==0返回0,否则返回1)
string String getString(int index)
String getNonResourceString(int index) 如果值是引用,返回null。(比如:@string/app_name,返回null)
fraction float getFraction(int index, int base, int pbase, float defValue) base:基数,pbase:%后面的p
reference int getResourceId(int index, int defValue)
color int getColor(int index, @ColorInt int defValue)
enum 跟获取int一样
flag 跟获取int一样

这里并不是全部方法,其它的可以阅读官方文档

关于float getFraction(int index, int base, int pbase, float defValue)参数:

  • index:检索属性的索引
  • base:这个分数的基本值。换句话说,一个标准分数乘以这个值。
  • pbase:这个分数的父值。换句话说,(nn % p)中的p的值。
  • defValue:如果属性没有定义或没有资源,则返回该值

返回值:属性分数值乘以适当的基值,如果没有定义就会返回defValue。

fraction的赋值格式是(nn % p),下面测试一下getFraction函数:声明customFraction1和customFraction2

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomView">
        <attr name="customFraction1" format="fraction" />
        <attr name="customFraction2" format="fraction" />
    </declare-styleable>
</resources>
<ojin.customview.CustomView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:customFraction1="50%"
    app:customFraction2="50%p" />
public CustomView(Context context, AttributeSet attrs) {
    super(context, attrs);
    //获得typedArray
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView);

    final int N = typedArray.getIndexCount();
    for (int i = 0; i < N; i++) {
        final int attr = typedArray.getIndex(i);
        switch (attr) {
            case R.styleable.CustomView_customFraction1:
                Log.e("oJin", typedArray.getFraction(attr, 20, 10, 0) + "");
                break;
            case R.styleable.CustomView_customFraction2:
                Log.e("oJin", typedArray.getFraction(attr, 20, 10, 0) + "");
                break;
        }
    }

    typedArray.recycle();//回收typedArray
}

输出结果:

E/oJin: 10.0
E/oJin: 5.0

如果赋值的时候%后面带了p(只能带小字母p),获取的值是5(50%*10),否则是10(20*50%)。
%后面带了p时不管base。


最后

怎样获取多个类型的属性?看两个源码例子:
1、View中

<attr name="background" format="reference|color" />
case com.android.internal.R.styleable.View_background:
    background = a.getDrawable(attr);//getDrawable
    break;

2、TextView中

<attr name="textColor" format="reference|color" />
case com.android.internal.R.styleable.TextAppearance_textColor:
    textColor = appearance.getColorStateList(attr);//getColorStateList
    break;

如果不明白,再看一个例子

<TextView  android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@string/app_name" />

我把textColor赋值成@string/app_name,编译没有报错,运行起来了,当加载这个布局时抛出了RuntimeException。
也就是说你可以乱取。

上一篇:从0开始,开发一款属于自己的Android Studio插件 下一篇:Git-1、简介与安装