Android自定义View构造函数详解

目录

  • 目录
  • 初始Custom View的构造函数
  • 生成Custom View的自定义属性
  • 在Custom View的构造函数中获取自定义属性
  • 设置自定义属性值
    • 在布局xml文件中为属性赋值
    • 在style中为属性赋值
    • 通过RstyledefStyle为属性赋值
    • 在Custom View所在的Activity的Theme中指定
  • 后记

初始Custom View的构造函数

之前写过一篇实现圆形进度条的博客(自定义圆形进度条),通常我们在实现Custom View的时候,都会先继承View并实现View的三个构造函数,例如:

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;

public class MyCustomView extends View {
    /** * 第一个构造函数 */
    public MyCustomView(Context context) {
        this(context, null);
    }

    /** * 第二个构造函数 */
    public MyCustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /** * 第三个构造函数 */
    public MyCustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // TODO:获取自定义属性
    }

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

网上有很多关于三个构造函数使用时机的说法,但是说法正确的却没有几家,这里正式的给大家科普一下:

  1. 在代码中直接new一个Custom View实例的时候,会调用第一个构造函数。这个没有任何争议。
  2. 在xml布局文件中调用Custom View的时候,会调用第二个构造函数。这个也没有争议。
  3. 在xml布局文件中调用Custom View,并且Custom View标签中还有自定义属性时,这里调用的还是第二个构造函数

也就是说,系统默认只会调用Custom View的前两个构造函数,至于第三个构造函数的调用,通常是我们自己在构造函数中主动调用的(例如,在第二个构造函数中调用第三个构造函数)。

至于自定义属性的获取,通常是在构造函数中通过obtainStyledAttributes函数实现的。这里先介绍一下如何生成Custom View的自定义属性。

生成Custom View的自定义属性

Custom View添加自定义属性主要是通过declare-styleable标签为其配置自定义属性,具体做法是:在res/values/目录下增加一个resources xml文件,示例如下(res/values/attrs_my_custom_view.xml):

<resources>
    <declare-styleable name="MyCustomView">
        <attr name="custom_attr1" format="string" />
        <attr name="custom_attr2" format="string" />
        <attr name="custom_attr3" format="string" />
        <attr name="custom_attr4" format="string" />
    </declare-styleable>
    <attr name="custom_attr5" format="string" />
</resources

在上述xml文件中,我们声明了一个自定义属性集MyCustomView,其中包含了custom_attr1,custom_att2,custom_attr3,custom_attr4四个属性。同时,我们还声明了一个独立的属性custom_attr5。

所有resources文件中声明的属性都会在R.attr类中生成对应的成员变量:

public final class R {
    public static final class attr {
        public static final int custom_attr1=0x7f010038;
        public static final int custom_attr2=0x7f010039;
        public static final int custom_attr3=0x7f01003a;
        public static final int custom_attr4=0x7f01003b;
        public static final int custom_attr5=0x7f010000;
    }
}

但是声明在标签中的属性,系统还会在R.styleable类中生成相关的成员变量:

public static final class styleable {
        public static final int[] MyCustomView = {
            0x7f010038, 0x7f010039, 0x7f01003a, 0x7f01003b
        };
        public static final int MyCustomView_custom_attr1 = 0;
        public static final int MyCustomView_custom_attr2 = 1;
        public static final int MyCustomView_custom_attr3 = 2;
        public static final int MyCustomView_custom_attr4 = 3;
}

可以看出,R.styleable.MyCustomView是一个数组,其中的元素值恰好就是R.attr.custom_attr1~R.attr.custom_attr4的值。而下面的MyCustomView_custom_attr1~MyCustomView_custom_attr4正好就是其对应的索引。

知道了这些之后,我们就可以来学习一下,如何在Custom View的构造函数中获取自定义属性的值了。

在Custom View的构造函数中获取自定义属性

在第三个构造函数中获取自定义属性的代码如下:

    public MyCustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
         TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyCustomView, defStyle, R.style.defStyleAttr);
        for (int i = 0; i < a.length(); i ++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.MyCustomView_custom_attr1:
                    String str1 = a.getString(attr);
                    break;
                case R.styleable.MyCustomView_custom_attr2:
                    String str2 = a.getString(attr);
                    break;
                case R.styleable.MyCustomView_custom_attr3:
                    String str3 = a.getString(attr);
                    break;
                case R.styleable.MyCustomView_custom_attr4:
                    String str4 = a.getString(attr);
                    break;
            }
        }
        a.recycle();
    }

关于自定义属性的获取,我们主要是调用了obtainStyledAttributes这个函数,我们来看一下这个函数的源码实现:

        public TypedArray obtainStyledAttributes(AttributeSet set,
                @StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
            final int len = attrs.length;
            final TypedArray array = TypedArray.obtain(Resources.this, len);

            // XXX note that for now we only work with compiled XML files.
            // To support generic XML files we will need to manually parse
            // out the attributes from the XML file (applying type information
            // contained in the resources and such).
            final XmlBlock.Parser parser = (XmlBlock.Parser)set;
            AssetManager.applyStyle(mTheme, defStyleAttr, defStyleRes,
                    parser != null ? parser.mParseState : 0, attrs, array.mData, array.mIndices);

            array.mTheme = this;
            array.mXml = parser;

            if (false) {
                int[] data = array.mData;

                System.out.println("Attributes:");
                String s = " Attrs:";
                int i;
                for (i=0; i<set.getAttributeCount(); i++) {
                    s = s + " " + set.getAttributeName(i);
                    int id = set.getAttributeNameResource(i);
                    if (id != 0) {
                        s = s + "(0x" + Integer.toHexString(id) + ")";
                    }
                    s = s + "=" + set.getAttributeValue(i);
                }
                System.out.println(s);
                s = " Found:";
                TypedValue value = new TypedValue();
                for (i=0; i<attrs.length; i++) {
                    int d = i*AssetManager.STYLE_NUM_ENTRIES;
                    value.type = data[d+AssetManager.STYLE_TYPE];
                    value.data = data[d+AssetManager.STYLE_DATA];
                    value.assetCookie = data[d+AssetManager.STYLE_ASSET_COOKIE];
                    value.resourceId = data[d+AssetManager.STYLE_RESOURCE_ID];
                    s = s + " 0x" + Integer.toHexString(attrs[i])
                        + "=" + value;
                }
                System.out.println(s);
            }

            return array;
        }

这里就不做过多的源码讲解,而是把这四个参数的含义解释给大家:

  1. AttributeSet set:属性值的集合。
  2. int[] attrs : 我们自定义属性集合在R类中生成的int型数组。这个数组中包含了自定义属性的资源ID。
  3. int defStyleAttr : 这是当前Theme中的包含的一个指向style的引用。当我们没有给自定义View设置declare-styleable资源集合时,默认从这个集合里面查找布局文件中配置属性值。传入0表示不像该defStyleAttr中查找默认值。
  4. int defStyleRes : 这个也是一个指向Style的资源ID,但是仅在defStyleAttr为0或者defStyleAttr不为0但Theme中没有为defStyleAttr属性赋值时起作用。

由于一个属性可以在很多地方对其进行赋值,包括:XML布局文件中、decalare-styleable、theme中等,它们之间是有优先级次序的,按照优先级从高到低排序如下:

属性赋值优先级次序表:
在布局xml中定义 > style定义 > defStyle指定的默认值 > 在Custom View所在的Activity的Theme中指定

为了让大家有更清楚更直观的了解,再接下来设置自定义属性的章节中,我将对custom_attr1~4这4个属性分别在上述四个地方进行定义,然后在Custom View的构造函数中获取它的值,从而看一下,优先级顺序是否和我们预期的一样。

设置自定义属性值

在布局xml文件中为属性赋值

在设置自定义属性之前,我们首先要在主Activity的布局文件中调用我们的Custom View,并且为其设置特定的属性。

主布局文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cv="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent">

    <love.com.progressbar.view.MyCustomView  android:layout_width="match_parent" android:layout_height="match_parent" cv:custom_attr1="custom_attr1_from_layout_xml" style="@style/MyCustomViewStyle"/>

</LinearLayout>

注意

在给自定义属性赋值时,首先需要增加自定义属性的命名空间,例如:xmlns:cv=”http://schemas.android.com/apk/res-auto”,Android Studio推荐使用res-auto,在Eclipse中需要使用Custom View所在的包名:xmlns:cv=”http://schemas.android.com/apk/love.com.progressbar”

这里,在布局文件中我们为custom_attr1赋值为:custom_attr1_from_layout_xml。

在style中为属性赋值

其次,自定义属性还可以在Style中进行赋值,所以这里我们在xml布局文件中还为MyCustomView增加一个自定义的style,style代码如下:

    <style name="MyCustomViewStyle"> <item name="custom_attr1">custom_attr1_from_style</item> <item name="custom_attr2">custom_attr2_from_style</item> </style>

这里我们再次对custom_attr1属性进行了赋值,同时我们对custom_attr2也进行了赋值。

小提示:
聪明的同学肯定都猜到我这样赋值的作用了,但是还是要简述一下:
对于custom_attr1,我们在xml布局文件、style、defStyle和theme中均进行赋值,那最终得到的结果必然能证实谁的优先级最高。
对于custom_attr2,我们在style、defStyle和theme中进行赋值,通过得到的结果我们能知道谁的优先级第二高。
对于custom_attr3和custom_attr4的赋值情况我就不多解释了,我相信大家都懂得!!

通过R.style.defStyle为属性赋值

第三,由于defStyle是我们在构造函数中调用obtainStyledAttributes方法传入的值,默认为0不指定。为了突出效果,我们首先需要定义一个defStyle,如下所示:

    <style name="defStyleAttr"> <item name="custom_attr1">custom_attr1_from_defStyleAttr</item> <item name="custom_attr2">custom_attr2_from_defStyleAttr</item> <item name="custom_attr3">custom_attr3_from_defStyleAttr</item> </style>

同时,需要注意,这个自定义属性,需要在第三个构造函数中的obtainStyledAttributes函数中进行赋值,具体方法如下:

TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyCustomView, defStyle, R.style.defStyleAttr);

在Custom View所在的Activity的Theme中指定

第四,是需要在Theme中增加自定义属性。我们先需要确认调用Custom View的Activity使用的是什么主题,可以通过AndroidManifest.xml获取答案:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="love.com.progressbar" >

    <application  android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" >
        <activity android:name=".ui.MainActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

通过AndroidManifest.xml文件,我们看到,application标签中为其所有的Activity设定了统一的theme,为AppTheme,所以我们需要修改AppTheme的定义,为其增加自定义属性赋值:

    <!-- 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="custom_attr1">custom_attr1_from_theme</item> <item name="custom_attr2">custom_attr2_from_theme</item> <item name="custom_attr3">custom_attr3_from_theme</item> <item name="custom_attr4">custom_attr4_from_theme</item> </style>

我们已经在四个地方设置了我们自定义属性的值,接下来就是编写Custom View的第三个构造函数,获取并打印自定义属性的值了。

public class MyCustomView extends View {
    private String customStr1;
    private String customStr2;
    private String customStr3;
    private String customStr4;



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

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

    public MyCustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyCustomView, defStyle, R.style.defStyleAttr);
        for (int i = 0; i < a.length(); i ++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.MyCustomView_custom_attr1:
                    customStr1 = a.getString(attr);
                    break;
                case R.styleable.MyCustomView_custom_attr2:
                    customStr2 = a.getString(attr);
                    break;
                case R.styleable.MyCustomView_custom_attr3:
                    customStr3 = a.getString(attr);
                    break;
                case R.styleable.MyCustomView_custom_attr4:
                    customStr4 = a.getString(attr);
                    break;
            }
        }
        a.recycle();

        // 打印自定义属性
        Log.e("TAG", "custom str1=" + customStr1 + "\n custom str2=" + customStr2 +
            "\n custom str3=" + customStr3 + "\n custom str4=" + customStr4);
    }

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

通过Log,我们发现,最终的结果和我们的预期是一样的:
Android自定义View构造函数详解_第1张图片

后记

通过上图,我们再次总结一下自定义属性的属性赋值优先级:

在布局xml中定义 > style定义 > defStyle指定的默认值 > 在Custom View所在的Activity的Theme中指定

你可能感兴趣的:(android,自定义view)