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属性。

该文件的根节点是,在节点下可以添加多个节点,在节点中通过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 使用和obtainStyledAttributes方法

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

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

要想能通过style或theme设置XML属性的值,需要在节点下添加节点,并在节点下定义

如下所示:

<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的区别

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

不同的是,节点会在 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。中的name加上里面的属性的name就组成了R.styleable中的MyView_customColor 和 MyView_customText,中间以下划线连接。如下图所示:

其中 R.styleable.MyView_customColor 对应 R.attr.customColor,R.styleable.MyView_customText 对应 R.attr.customText。

再来看下直接在 下面定义,在 R.attr 中会发现有 customText 和 customColor,如下图所示:

R.attr.customText 和 R.attr.customColor 分别是属性 customText 和 customColor 的资源ID。

2.3.2.2 obtainStyledAttributes的使用

中定义的,在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:

  • 表示