自定义View中如果想通过XML文件指定参数,会直接在Res文件下新建的attr.xml,但是子节点有时候用styleable,有时候用attr。以至于对于两个一直有点傻傻分不清,今天搜索研究了几篇博客,算是有了一些眉目,所以在此记录下来,希望对看到博客的人有所帮助。

attr和styleable的关系

<resources>
    <attr name="customColor" format="color"/>
    <attr name="customText" format="string"/>
</resources>
<resources>
 <declare-styleable name="customView">
    <attr name="customColor" format="color"/>
    <attr name="customText" format="string"/>
    </declare-styleable>
</resources>

我们先捋清楚这两个节点的关系,这两个节点,无论使用哪个,所达到的效果都是一样的。attr不依赖于styleable,styleable只是为了方便attr的使用。

每定义一个attr,就会在R文件中生成一个id,我们去调用的时候会用R.attr.customAttr操作。
而通过定义一个styleable,可以在R文件里自动生成一个int[],数组的每个值对应的就是定义在styleable中的attr的id.

由此得知,定义一个declare-styleable,在获取属性的时候为我们自动提供了一个属性数组。此外,使用declare-styleable的方式有利于我们我们把相关的属性组织起来,有一个分组的概念,属性的使用范围更加明确。

为View添加自定义XML属性

<TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

如上是一个TextView的XML属性,我们可以通过android:text为TextView的文本赋值。

我们有时在自定义View的时候也需要自定义View的XML属性。

假设我们有一个自定义的View,其类名是com.cxx.demo.widget.MyTextView,其中com.cxx.demo.widget是应用程序的包名。

我们想要自定义XML属性,总的来说包括三步:
1. 在XML资源文件中定义需要的attr,指定attr的数据类型;
2. 在自定义View的构造函数中解析这些从XML中第一段属性值,将其存放到自定义View对应的成员变量中;
3. 在XML布局文件中为自定义View的XML属性赋值。

  • 我们需要在res/values目录下新建名字为attrs.xml文件(名字可以任意)。然后在该文件中定义MyTextView的XML属性。该文件的根节点为<resources>,我们在<resources>节点下可以添加多个<attr>子节点,通过name指定属性名称,通过format指定属性值的类型。如图所示:
  • android中xml设置斜体怎样做 安卓xml属性_R.styleable

  • 当我们指定了XML属性的名称和属性值的类型后,还需要先在布局文件中声明命名空间,studio的命名空间一般都指定为xmlns:app="http://schemas.android.com/apk/res-auto",这样定义的命名空间自动指向当前App的命名空间。
    在定义app的命名空间后,我们就可以为MyTextView属性赋值了。如果app:customColor指定的format类型为color,那么对应的XML属性值必须为color类型。如图所示:
  • android中xml设置斜体怎样做 安卓xml属性_android_02

format支持的类型有enum、boolean、color、dimension、flag、float、fraction、integer、reference、string。

format值

attr对应值类型

boolean

布尔类型的值,取值只能是true或false。

color

颜色类型的值,例如#ff0000,也可以使用一个指向Color的资源,

比如@android:color/background_dark,但是不能用0xffff0000这样的值。

string

字符串类型。

integer

整数类型,取值只能是整数,不能是浮点数。

float

浮点数类型,取值只能是浮点数或整数。

fraction

百分数类型,取值只能以%结尾,例如30%、120.5%等。

dimension

尺寸类型,例如取值16px、16dp,也可以使用一个指向<dimen>类型的资源,

比如@android:dimen/app_icon_size

reference

只能指向某一资源的ID,例如取值@id/textView。

enum

枚举类型,在定义enum类型的attr时,可以将attr的format设置为enum,

也可以不用设置attr的format属性,但是必须在attr节点下面添加一个或多个enum节点。

flag

bit位标记,flag与enum有相似之处,定义了flag的attr,在设置值时,可以通过| 设置多个值,而且每个值都对应一个bit位,这样通过按位或操作符|可以将多个值合成一个值,我们一般在用flag表示某个字段支持多个特性,需要注意的是,要想使用flag类型,不能在attr上设置format为flag,不要设置attr的format的属性,直接在attr节点下面添加flag节点即可。

<!--enum举例-->
<attr name="customAttr">
    <enum name="man" value="0" />
    <enum name="woman" value="1" />
</attr>
<!--flag举例-->
<attr name="customAttr">
    <flag name="none" value="0" />
    <flag name="bold" value="0x1" />
    <flag name="italic" value="0x2" />
    <flag name="underline" value="0x4" />
</attr>

在<attr>节点下通过定义多个<flag>表示其支持的值,value的值一般是0或者是2的N次方(N为大于等于0的整数),
对于上面的例子我们在实际设置值是可以设置单独的值,如none、bold、italic、underline,也可以通过|设置多个值,
例如app:customAttr="italic|underline"。

自定义View的代码为:

package com.cxx.demo.widget;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;

import com.cxx.demo.R;

/**
 * Created by ZHOU on 2018/1/18.
 */

public class MyTextView extends android.support.v7.widget.AppCompatTextView {

    //存储要显示的文本
    private String mCustomText;

    /*** 存储文本的显示颜色*/

    private int mCustomColor = 0xFF000000;
    //画笔
    private TextPaint mTextPaint;
    //字体大小
    private float fontSize = getResources().getDimension(R.dimen.textSize);


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

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

    public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs, defStyleAttr);
    }

    private void init(AttributeSet attrs, int defStyleAttr) {
     //首先判断attrs是否为null
        if (attrs!=null){
        //获取AttributeSet中所有的XML属性的数量
            int count  = attrs.getAttributeCount();
              //遍历AttributeSet中的XML属性
            for (int i = 0; i < count; i++) {
                  //获取attr的资源ID
                int attrsResId = attrs.getAttributeNameResource(i);
                switch (attrsResId){
                    case R.attr.customColor:
                    //customColor属性
                        //如果读取不到对应的颜色值,那么就用黑色作为默认颜色
                        mCustomColor = attrs.getAttributeIntValue(i,0xff000000);
                        break;
                    case R.attr.customText:
                      //customText属性
                        mCustomText = attrs.getAttributeValue(i);
                        break;
                    default:
                        break;
                }
            }
        }
     //初始化画笔
        mTextPaint = new TextPaint();
        mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextSize(fontSize);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!TextUtils.isEmpty(mCustomText)){
            mTextPaint.setColor(mCustomColor);
            //将文本绘制显示出来
            canvas.drawText(mCustomText,0,fontSize,mTextPaint);
        }
    }
}

我们在MyTextView 中定义了两个成员变量mCustomText和mCustomColor。MyTextView无论调用哪个构造函数最终都会调用到init方法,我们重点看一下init方法。

  • 传递给init方法的是一个AttributeSet对象,可以把它看成一个索引数组,这个数组里面存储着MyTextView在XML中定义属性的索引,通过索引可以得到XML属性名和属性值。
  • 通过调用AttributeSet的getAttributeCount()方法可以获得XML属性的数量,然后我们就可以在for循环中通过索引遍历AttributeSet的属性名和属性值。AttributeSet中有很多getXXX方法,一般必须的参数都是索引号,说几个常用的方法:

方法

解释

getAttributeName (int index)

得到对应索引的XML属性名

getAttributeNameResource (int index)

得到对应索引的XML属性在R.attr中的资源ID,例如R.attr.customText、R.attr.customColor。

getAttributeValue (int index)

得到对应索引的XML属性值

如果index对应的XML属性的format是string,那么通过AttributeSet的String getAttributeValue (int index)方法,可以得到对应索引的XML属性的值,该方法返回的是String。除此之外,AttributeSet还有getAttributeIntValue、getAttributeFloatValue、getAttributeListValue等方法,返回不同类型的属性值。

  • 这样就可以将XML中定义的属性获取并赋值给成员变量以供使用了。

使用<declare-styleable>和obtainStyledAttributes方法

我们上面定义的customText和customColor这两个<attr>属性都是直接在<resources>节点下定义的,这样定义<attr>属性存在一个问题:不能通过style或theme设置这两个属性的值。
要想能够通过style或theme设置XML属性的值,需要在<resources>节点下添加<declare-styleable>节点,并在<declare-styleable>节点下定义<attr>,如下所示:

<resources>
    <declare-styleable name="MyTextView">
        <attr name="customText" format="string" />
        <attr name="customColor" format="color" />
    </declare-styleable>
</resources>

<!-- 需要给<declare-styleable>设置name属性,一般name设置为自定义View的名字,我们此处设置为MyTextView。-->

R.styleable.MyTextView是一个int数组,也就是R.styleable.MyTextView等价于数组[R.attr.customText, R.attr.customColor]。
<declare-styleable>中定义的<attr>在MyTextView中需要通过调用obtainStyledAttributes()方法来读取解析属性值,其中,Resources.Theme.obtainStyledAttributes有三个重载方法,如下所示:

  • public TypedArray obtainStyledAttributes (int[] attrs)
  • public TypedArray obtainStyledAttributes (int resid, int[] attrs)
  • public TypedArray obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)

Resources有一个重载方法

  • public TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs)

这几个方法都是返回一个TypedArray对象,这里我们用的是四个参数的方法。如下所示:

private void init(AttributeSet attributeSet, int defStyle) {
        //首先判断attributeSet是否为null
        if(attributeSet != null){
            //获取当前MyView所在的Activity的theme
            Resources.Theme theme = getContext().getTheme();
            //通过theme的obtainStyledAttributes方法获取TypedArray对象
            TypedArray typedArray = theme.obtainStyledAttributes(attributeSet, R.styleable.MyTextView, 0, 0);
            //获取typedArray的长度
            int count = typedArray.getIndexCount();
            //通过for循环遍历typedArray
            for(int i = 0; i < count; i++){
                //通过typedArray的getIndex方法获取指向R.styleable中对应的属性ID
                int styledAttr = typedArray.getIndex(i);
                switch (styledAttr){
                    case R.styleable.MyView_customText:
                        //如果是R.styleable.MyView_customText,表示属性是customText
                        //通过typedArray的getString方法获取字符串值
                        mCustomText = typedArray.getString(i);
                        break;
                    case R.styleable.MyView_customColor:
                        //如果是R.styleable.MyView_customColor,表示属性是customColor
                        //通过typedArray的getColor方法获取整数类型的颜色值
                        mCustomColor = typedArray.getColor(i, 0xFF000000);
                        break;
                }
            }
            //在使用完typedArray之后,要调用recycle方法回收资源
            typedArray.recycle();
        }

        ...
    }

TypedArray是一个数组,通过该数组可以获取应用了style和theme的XML属性值。我们看上面的obtainStyledAttributes()方法,后面两个参数暂且忽略不计,后面会介绍。第一个参数还是AttributeSet对象,第二个参数是一个int类型的数组,该数组表示想要获取的属性值的属性的R.attr中的ID,此处我们传入的是R.styleable.MyTextView,在上面我们已经提到其值等价于[R.attr.customText, R.attr.customColor],表示我们此处想获取customText和customColor这两个属性的值。

注:TypedArray其实是用来简化我们的工作的,比如上例,如果布局中的属性的值是引用类型(比如:@dimen/dp100),如果使用AttributeSet去获得最终的像素值,那么需要第一步拿到id,第二步再去解析id。而TypedArray会直接去dp100的值,TypedArray正是帮我们简化了这个过程。

<com.cxx.demo.widget.MyTextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:customText="customText in AttributeSet"
        style="@style/RedStyle"
        />
<style name="RedStyle">
        <item name="customText">customText in RedStyle</item>
        <!-- 红色 -->
        <item name="customColor">#FFFF0000</item>
    </style>

View的style属性对应的style资源中定义的XML属性值,其实是View直接在layou文件中定义XML属性值的替补值,是用于补漏的,AttributeSet(即在layout中直接定义XML属性)的优先级高于style属性中资源所定义的属性值。

obtainStyledAttributes方法之defStyleAttr

方法obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)中的第三个参数defStyleAttr,这个参数表示的是一个<style>中某个属性的ID( R.attr.***),当Android在AttributeSet和style属性所定义的style资源中都没有找到XML属性值时,就会尝试查找当前theme(theme其实就是一个<style>资源)中属性为defStyleAttr的值,如果其值是一个style资源,那么Android就会去该资源中再去查找XML属性值。

obtainStyledAttributes方法之defStyleRes

方法obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)中的第四个参数defStyleRes。这个参数表示的是一个<style>(R.style.***)。与defStyleAttr类似,defStyleRes是前面几项的替补值,defStyleRes的优先级最低。与defStyleAttr不同的是,defStyleRes本身直接表示一个style资源,而theme要通过属性defStyleAttr间接找到style资源。

总结

  • 可以不通过<declare-styleable>节点定义XML属性,不过还是建议将XML属性定义在<declare-styleable>节点下,因为这样Android会在R.styleable下面帮我们生成很多有用的常量供我们直接使用。
  • obtainStyledAttributes方法中,优先级从高到低依次是:直接在layout中设置View的XML属性值(AttributeSet) > 设置View的style属性 > defStyleAttr > defStyleRes