Android -- 自定义 View XML属性详解

1. View的构造函数

自定义View必须重写至少一个构造函数

   // 如果View是在Java代码里面new的,则调用第一个构造函数
    public CustomView7(Context context) {
        super(context);
    }

    // 如果View是在.xml里声明的,则调用第二个构造函数
    public CustomView7(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // 如View有style属性时
    public CustomView7(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //API21之后才使用
    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // 如View有style属性时
    public CustomView7(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

下面对构造函数的参数使用进行详解

2. 为View添加自定义XML属性

Android中的各种Widget都提供了很多XML属性,我们可以利用这些XML属性在layout文件中为Widget的属性赋值。

如下所示:

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

我们可以通过TextView所提供的的XML属性android:text为TextView的文本赋值。

在自定义View的时候也会经常需要自定义View的XML属性。

想要自定义XML属性,总的来说包括三步:

  1. 在xml资源文件中定义各种attr,并指定attr的数据类型。
  2. 在自定义View的构造函数中解析这些从XML中定义的属性值,将其存放到View对应的成员变量中。
  3. 在layout文件中为自定义View的XML属性赋值。

下面围绕这三步对 自定义View XML属性 进行详解。

假设我们有一个自定义View,其类名是 com.example.jjzg.customview.view.CustomView6,其中 com.example.jjzg是应用程序的包名。
CustomView6直接继承自View,想让CustomView6可以显示文本,即传递文本给CustomView6,相当于非常简单的TextView。

2.1 在xml资源文件中定义attr

首先,在res/values目录新建一个名为attrs.xml文件(文件名可随意命名,只要是xml文件就行),在该文件中定义CustomView6所支持的XML属性。

该文件的根节点是<resources>,在<resources>节点下可以添加多个<attr>节点,在<attr>节点中通过name指定XML属性名称,通过format指定XML属性值的类型。
在这里插入图片描述
由上图可知,·format支持多种类型。

format支持的类型有:

  • enum
    表示attr是枚举类型。
    在定义attr时,可以将attr的format设置为enum,也可以不用设置attr的format属性,但必须在attr节点下添加一个或多个enum节点,如下所示:

    <attr name="customAttr">
        <enum name="man" value="0" />
        <enum name="woman" value="1" />
    </attr>
    

    这样attr的属性值只能取man或woman了。

  • boolean
    表示attr是布尔类型的值,取值只能是true或false。

  • color
    表示attr是类型颜色,例如#ff0000,也可以使用一个指向Color的资源,如@android:color/background_dark,但是不能用0xffff0000这样的值。

  • dimension
    表示attr是尺寸类型,如取值16px、16dp,也可以使用一个指向dimen类型的资源,如@android:dimen/app_icon_size。

  • float
    表示attr是浮点数类型,取值只能是浮点数或整数。

  • fraction
    表示attr是百分数类型,取值只能以%结尾,例如30%、120.5%等。

  • integer
    表示attr是整数类型,取值只能是整数,不能是浮点数。

  • string
    表示attr是字符串类型。

  • reference
    表示attr的值只能指向某一资源的ID,如取值@id/textView。(下面的例子中会使用到)

  • flag
    表示attr是bit位标记。
    flag和enum有相似之处,定义了flag的attr,在设置值时,可以通过|设置多个值,而且每个值都对应一个bit位,这样通过按位或操作符|可以将多个值合成一个值。
    一般在用flag表示某个字段支持多个特性,需要注意的是:要想使用flag类型,不要设置attr的format的属性,直接在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>
    

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

根据上面的需求,要传递文本给CustomView6,所以在attrs.xml文件里定义两个属性,文本和文本颜色:

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

2.2 在layout文件中为自定义View的XML属性赋值

当指定了XML属性的名称和属性值的类型后,就可以在layout文件中通过XML属性为其赋值了。
在这里插入图片描述
通过com.example.jjzg.customview.view.CustomView6在layout中引入了CustomView6,为了能够使用自定义属性,通常要指定一个 自定义的命名空间 以区别于Android的命名空间xmlns:android

2.2.1 自定义的命名空间

自定义命名空间的名字可以是任意的,通常一般用xmlns:app

不同的开发软件,自定义命名空间的写法也是不一样的:

  • Eclipse开发工具
    可以这样定义命名空间xmlns:app=http://schemas.android.com/apk/res/com.example.jjzg,其中com.example.jjzg是应用程序的包名。
  • Android Studio开发工具
    如果在Android Studio中采用Eclipse那种命名空间的格式,是有问题的。Android Studio使用Gradle进行build,而Gradle不允许自定义的命名空间以包名结尾。正确格式:xmlns:app="http://schemas.android.com/apk/res-auto",这样定义的命名空间自动指向当前App的命名空间。

2.2.2 为XML属性赋值

在正确定义app命名空间之后,就可以用app:customText_6为CustomView6的customText_6属性赋值了。

2.3 在自定义View的构造函数中解析XML中定义的属性值

在自定义View的构造函数中解析这些从XML中定义的属性值,将其存放到View对应的成员变量中。

2.3.1 基本用法

前面已经在attrs.xml文件中对自定义控件CustomView6定义了属性,并在layout文件中对其属性进行了赋值,接下来看下自定义CustomView6的具体实现:

package com.example.jjzg.customview.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;

import com.example.jjzg.R;

public class CustomView6 extends View {
    //存储要显示的文本
    private String mCustomText;
    //存储文本的显示颜色
    private int mCustomColor = Color.RED;
    //画笔
    private Paint mTextPaint;
    //字体大小
    private float fontSize = getResources().getDimension(R.dimen.font_size_16);

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

    public CustomView6(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void init(AttributeSet attrs){
        if (attrs != null){
            //获取AttributeSet中所有的XML属性的数量
            int count = attrs.getAttributeCount();
            //遍历AttributeSet中的XML属性
            for (int i = 0; i < count; i++){
                //获取attr的资源ID
                int attrResId = attrs.getAttributeNameResource(i);
                switch (attrResId){
                    case R.attr.customText_6:
                        //customText属性
                        mCustomText = attrs.getAttributeValue(i);
                        break;
                    case R.attr.customColor_6:
                        //customColor属性
                        //如果读取不到对应的颜色值,那么就用红色作为默认颜色
                        mCustomColor = attrs.getAttributeIntValue(i, Color.RED);
                        break;
                }
            }

            mTextPaint = new Paint();
            mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
            mTextPaint.setTextSize(fontSize);
            mTextPaint.setColor(mCustomColor);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!TextUtils.isEmpty(mCustomText)){
            canvas.drawText(mCustomText, 0, fontSize, mTextPaint);
        }
    }
}

在CustomView6中定义了两个成员变量mCustomText和mCustomColor,CustomView6的几个构造函数都会调用init方法,这里重点看下init方法:

  • 传递给init方法的是一个 AttributeSet 对象,可以把它看成是一个索引数组,这个数组里存储着属性的索引,通过索引可以得到XML属性名和属性值。

  • 通过调用 AttributeSe t的 getAttributeCount() 方法可以获得XML属性的数量,之后就可以在for循环中通过索引遍历 AttributeSet 的属性名和属性值。
    AttributeSet 中有很多getXXX方法,一般必须的参数都是索引号,说几个常用的方法:

    • 通过 AttributeSet 的public abstract String getAttributeName (int index)方法可以得到对应索引的XML属性名。
    • 通过 AttributeSet 的public abstract int getAttributeNameResource (int index)方法可以得到对应索引的XML属性在R.attr中的资源ID,例如 R.attr.customText、R.attr.customColor。
    • 如果index对应的XML属性的format是string,那么通过 AttributeSet 的public abstract String getAttributeName (int index)方法可以得到对应索引的XML属性的值,该方法返回的是String。
      除此之外,AttributeSet 还有 getAttributeIntValue、getAttributeFloatValue、getAttributeListValue 等方法,返回不同类型的属性值。
  • 通过 attrs.getAttributeNameResource(i) 得到获取 attr 的资源 ID,然后对attrResId进行switch判断:

    • 如果是 R.attr.customText,表示当前属性是customText,通过 attrs.getAttributeValue(i) 读取customText属性值,并将其赋值给成员变量 mCustomText。
    • 如果是 R.attr.customColor,表示当前属性是customColor,由于Android中用一个4字节的int型整数表示颜色,所以我们通过 attrs.getAttributeIntValue(i, 0xFF000000) 读取了customColor的颜色值,并将其赋值给成员变量mCustomColor。
  • 重写onDraw方法,把画笔mTextPaint的颜色设置为mCustomColor颜色,通过执行canvas.drawText()将mCustomText绘制到界面上。这样,CustomView6就使用了customText和customColor这两个XML属性。

运行效果如下:
在这里插入图片描述

2.3.2 使用<declare-styleable>和obtainStyledAttributes方法

在上面的attrs.xml文件中,customText和customColor这两个<attr>属性都是直接在<resources>节点下定义的。

这样定义<attr>属性存在一个问题:不能通过style或theme设置这两个属性的值

要想能通过style或theme设置XML属性的值,需要在<resources>节点下添加<declare-styleable>节点,并在<declare-styleable>节点下定义<attr>

如下所示:

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

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

2.3.2.1 不同节点下定义attr的区别

<declare-styleable>下面定义<attr>属性 与 直接在 <resources>下面定义<attr>属性 其 本质上没有太大区别,无论哪种方式定义<attr>,都会在 R.attr 类中定义 R.attr.customText 和 R.attr.customColor。

不同的是,<declare-styleable name="MyView">节点会在 R.styleable 这个内部类中有如下定义:
在这里插入图片描述
R.styleable.MyView是一个int数组,其值为0x7f010038和 0x7f010039。0x7f010038就是属性R.attr.customText,0x7f010039就是属性R.attr.customColor。也就是R.styleable.MyView等价于数组[R.attr.customText, R.attr.customColor]。

同样可以在 R.styleable 中发现 R.styleable.MyView_customColor 和 R.styleable.MyView_customText 这两个ID。<declare-styleable name="MyView">中的name加上里面的属性的name就组成了R.styleable中的MyView_customColor 和 MyView_customText,中间以下划线连接。如下图所示:
在这里插入图片描述
其中 R.styleable.MyView_customColor 对应 R.attr.customColor,R.styleable.MyView_customText 对应 R.attr.customText。

再来看下直接在 <resources>下面定义<attr>,在 R.attr 中会发现有 customText 和 customColor,如下图所示:
在这里插入图片描述
R.attr.customText 和 R.attr.customColor 分别是属性 customText 和 customColor 的资源ID。

2.3.2.2 obtainStyledAttributes的使用

<declare-styleable>中定义的<attr>,在MyView中需要通过调用obtainStyledAttributes方法来读取解析属性值。

obtainStyledAttributes有三个重载方法,分别如下所示:

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

直接来看第三个最复杂的方法。

先在res/valeus/styles.xml文件中定义style:

   <style name="YellowStyle">
        <item name="customText">customText in YellowStyle</item>
        <item name="customColor">#FFEB3B</item>
    </style>

然后在layout文件中将CustomView7的style属性设置为上面的style:

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

    <com.example.jjzg.customview.view.CustomView7
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:customText="customText in AttributeSet"
        style="@style/YellowStyle"/>
</LinearLayout>

接着看下CustomView7:
(CustomView7 和 CustomView6 的代码基本相同,唯一的不同点在init方法中,这里主要对init方法进行详解)

...
private void init(Context context, AttributeSet attrs, int defStyleAttr){
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView,
                0, 0);
        mCustomText = typedArray.getString(R.styleable.MyView_customText);
        mCustomColor = typedArray.getColor(R.styleable.MyView_customColor, Color.RED);
        typedArray.recycle();

        mTextPaint = new Paint();
        mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextSize(fontSize);
        mTextPaint.setColor(mCustomColor);
    }
 ...

运行效果如下:
在这里插入图片描述
解析init方法:

  • 通过context.obtainStyledAttributes(attrs, R.styleable.MyView, 0, 0)方法返回TypedArray对象。
    TypedArray是一个数组,通过该数组可以获取应用style和theme的XML属性值。
    上面这个方法有四个参数,后面两个参数都是0,大家暂且忽略不计,后面会介绍。
    参数1:AttributeSet对象,存放自定义XML属性值
    参数2:int类型的数组,该数组表示想要获取属性值的属性的R.attr中的ID,此处我们传入的是R.styleable.MyView,在上面我们已经提到其值等价于[R.attr.customText, R.attr.customColor],表示我们此处想获取customText和customColor这两个属性的值。
  • 如果在layout文件中直接为CustomView7设置了某些XML属性,那么这些XML属性及其属性值就会出现在AttributeSet中,那Android就会直接使用AttributeSet中该XML属性值作为context.obtainStyledAttributes的返回值
    如上面的例子中,通过app:customText="customText in AttributeSet"设置了CustomView7的XML属性,最终运行的效果显示的也是文本”customText in AttributeSet”。
  • 如果在layout文件中没有为CustomView7设置某个XML属性,但是给其设置了style属性,例如style="@style/RedStyle",并且在style中指定了相应的XML属性,那么Android就会用style属性所对应的style资源中的XML属性值作为context.obtainStyledAttributes的返回值
    如上面的例子中,我们在layout文件中没有设置app:customColor的值,但是在其style属性所对应的YellowStyle资源中将customColor设置成了黄色,最终文本也是以黄色显示在界面上的。
小结:

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

2.3.2.3 obtainStyledAttributes方法之defStyleAttr

上面讲了obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)方法中的两个参数,再来看下第三个参数defStyleAttr。

defStyleAttr:

  • 表示<style>中某个属性的ID
  • 当在AttributeSet和style属性所定义的style资源中都没有找到XML属性值时,就会查找当前theme(theme其实就是一个<style>资源)中属性为defStyleAttr的值,如果其值是一个style资源,那Android就会去该资源中再去查找XML属性值。

听起来比较费劲,下面举例说明。

更改values/styles.xml文件:

<resources>

    <!-- Base application theme. -->
    <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="myViewStyle">@style/GreenStyle</item>
    </style>

    <style name="GreenStyle">
        <item name="customText">customText in GreenStyle</item>
        <item name="customColor">#FF00FF00</item>
    </style>

    <style name="HelloWorldStyle">
        <item name="customText">customText in HelloWorldStyle</item>
    </style>

    <style name="YellowStyle">
        <item name="customText">customText in YellowStyle</item>
        <item name="customColor">#FFEB3B</item>
    </style>

</resources>

这里添加了HelloWorldStyle和GreenStyle,其中HelloWorldStyle只定义了customText的属性值,而GreenStyle同时定义了customText和customColor的值。

在AppTheme中,设置了myViewStyle这个属性的值:

<item name="myViewStyle">@style/GreenStyle</item>

myViewStyle这个属性是在values/attrs.xml中定义的:

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

myViewStyle被定义为一个reference格式,即指向一个资源类型。

我们在AppTheme中将其赋值为@style/GreenStyle,即在AppTheme中,myViewStyle的就是GreenStyle,其指向了一个style资源。

更改CustomView7中的init方法:

TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView,
                R.attr.myViewStyle, 0);

注意,上面obtainStyledAttributes方法最后一个参数还是为0,可以忽略,但是第三个参数的值不再是0,而是R.attr.myViewStyle。

然后更新layout文件:

<com.example.jjzg.customview.view.CustomView7
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        style="@style/HelloWorldStyle"/>

先看下运行效果:
在这里插入图片描述
显示的是绿色的customText in HelloWorldStyle,分析下:

  • 由于这次没有通过layout文件直接设置CustomView7的XML属性的值,所以AttributeSet本身是没有XML属性值的,我们直接忽略掉AttributeSet。
  • 通过style="@style/HelloWorldStyle"为CustomView7设置了style为HelloWorldStyle,HelloWorldStyle中定义customText的属性值为“customText in HelloWorldStyle”,所以最终customText的值就是”customText in HelloWorldStyle”,在界面上显示的也是该值。
  • HelloWorldStyle中并没有定义customColor的属性值。
    将 context.obtainStyledAttributes 方法的第三个参数设置为R.attr.myViewStyle,此处的theme就是上面提到的AppTheme,Android会去AppTheme中查找属性为myViewStyle的值,之前提到了,它的值就是@style/GreenStyle,即GreenStyle,由于该值是个style资源,Android就会去该资源中查找customColor的值,GreenStyle定义了customColor的颜色为绿色,所以CustomView7最终所使用的customColor的值就是绿色。
小结:

此处的第三个参数的作用是:当在AttributeSet和style属性中都没有找到属性值时,就去Theme的某个属性(即第三个参数)中查看其值是否是style资源,如果有style资源再去这个style资源中查找XML属性值作为替补值

2.3.2.4 obtainStyledAttributes方法之defStyleRes

最后看一下方法obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)中的第四个参数defStyleRes。

与defStyleAttr类似,defStyleRes是前面几项的替补值,defStyleRes的优先级最低。
与defStyleAttr不同的是,defStyleRes本身直接表示一个style资源,而theme要通过属性defStyleAttr间接找到style资源。

在values/styles.xml文件中添加 BlueStyle这个style:

<resources>

    <!-- Base application theme. -->
    <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="myViewStyle">@style/GreenStyle</item>
    </style>
    
    <style name="BlueStyle">
        <item name="customText">customText in BlueStyle</item>
        <item name="customColor">#FF0000FF</item>
    </style>

    <style name="GreenStyle">
        <item name="customText">customText in GreenStyle</item>
        <item name="customColor">#FF00FF00</item>
    </style>

    <style name="HelloWorldStyle">
        <item name="customText">customText in HelloWorldStyle</item>
    </style>

    <style name="YellowStyle">
        <item name="customText">customText in YellowStyle</item>
        <item name="customColor">#FFEB3B</item>
    </style>

</resources>

将layout文件改为如下所示:

<com.example.jjzg.customview.view.CustomView7
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

更改CustomView7中的init方法:

TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView,
               0, R.style.BlueStyle);

第三个参数设置为0,第四个参数不再是0,而是R.style.BlueStyle。

运行界面如下所示:
在这里插入图片描述

小结:

当defStyleAttr(即View的构造函数的第三个参数)不为0且在Theme中有为这个attr赋值时,defStyleRes(通过obtainStyledAttributes的第四个参数指定)不起作用。

3. 总结

  • 可以不通过<declare-styleable>节点定义XML属性,不过还是建议将XML属性定义在<declare-styleable>节点下,这样Android会在R.styleable下面帮我们生成很多有用的常量供我们直接使用。
  • 在obtainStyledAttributes方法中,优先级从高到低依次是:
    直接在layout中设置View的XML属性值(AttributeSet)> 设置View的style属性 > defStyleAttr > defStyleRes
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值