Android学习笔记---自定义View#01

最近发现自己对Android的学习只在表面,并没有深入的理解,我不喜欢这种感觉,而且没有自己的理解,学习到的内容也很难为我所用.

所以从本次开始,我要写点自己理解的东西,但要对知识有自己的理解,那就必须深入了解它的原理.而我觉得Android的自定义View是一个很好的入口.

学会如何自定义View,能够了解Android系统中View使如何创建和维护的.这有助于我们学习Android的View的基本机制,也能解决我们日常开发的需求.

接下来我们一起来对自定义View中的各个部分做详细的研究,现在我们先从View的构造函数入手.

自定义View的构造函数

自定义View最基础的方式就是创建一个继承于View或其子类的类,并且最少重写父类的一个构造函数.

/**
 */
public class MyView extends View {
    /**
     * 在代码中使用new关键字创建View时会调用
     * @param context
     */
    public MyView(Context context) {
        super(context);
    }

    /**
     * 在layout文件声明View时会调用,只有实现了这个构造函数才能在布局文件中声明此自定义View
     * @param context
     * @param attrs 存有View在xml布局文件中的自定义的属性
     */
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 与第二个构造函数不同的地方在于,此构造函数可以指定View的默认样式
     * @param context
     * @param attrs
     * @param defStyleAttr 是当前theme中包含的指向View的样式资源的属性(0代表此参数无效)
     */
    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

第一个构造函数比较简单,我们从第二个构造函数开始研究.
当我们需要在xml布局文件中使用我们的View时,我们就必须实现第二个构造函数.第二个构造函数的参数列表为MyView(Context context, AttributeSet attrs),这里的context不用多说,就是View所在的上下文,而第二个参数就是AttributeSet类型的一个Set集合.它是一个保存了View的自定义属性的集合,即我们在xml布局文件中为View所设置的属性可以通过这个参数获取.
通常我们会在res/values/文件下创建一个attrs.xml文件来声明我们的自定义属性.该文件是一个资源文件.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyView">
        <attr name="myview_text" format="string"/>
    </declare-styleable>
</resources>

上面我们在attrs.xml中定义了我们的自定义属性,其中<declare-styleable>标签表示一组自定义的属性,对应着一个View的自定义属性.其中的name可以是任何值,但为了方便和规范,建议name与对应的View同名.
<attr>标签就是一个具体的属性了,它的name代表该属性的名字.format代表该属性的值的格式.者两个是必须要有的.<attr>标签支持的formatstring,enum,boolean,dimension,color,float...等多种格式.

有了自定义的属性后,在xml布局文件中使用View时就能使用我们的自定义属性了.但我们需要在使用自定义属性前,指定自定义属性的命名空间,这样系统才能准确的找到你的自定义属性.
而它们的命名空间为http://schemas.android.com/apk/res/[your package name],这里不同的View可能会有不同的package name,这样一来就比较麻烦.但是在Android Studio中我们只需指定一个统一的命名空间即可http://schemas.android.com/apk/res-auto.剩下的AS会帮我们完成.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
    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"
    >

    <com.source.kevin.costomviewlib.costomview.MyView
        app:myview_text="Hello"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</RelativeLayout>

为了验证第二个参数AttributeSet能否得到我们的自定义属性,我们在第二个构造函数中测试一下.

    /**
     * 在layout文件声明View时会调用,只有实现了这个构造函数才能在布局文件中声明此自定义View
     * @param context
     * @param attrs 存有View在xml布局文件中的自定义的属性
     */
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
        try{
            String string = a.getString(R.styleable.MyView_myview_text);
            Log.e("RESULT",string);
        }finally {
            a.recycle();//回收资源
        }
    }

先运行一下应用,然后在控制台的Log中我们看到了输出

《Android学习笔记---自定义View#01》

我们很简单的就得到了自定义的属性.代码中我们使用了
context.obtainStyledAttributes(AttributeSet set, @StyleableRes int[] attrs)这个函数获取我们的自定义属性集合.返回的是一个
TypeArray类型的对象,这个对象包含了View的自定义属性.第1个参数便是要解析的
AttributeSet,第2个参数
int[] attrs就是我们
attrs.xml文件中定义的一组属性资源.其实我们可以打开自动生成的
R.java文件,在文件中搜索
MyView关键字,定位到相应的行数,我们可以看到下面的代码:

....
public static final class attr {
....
    public static final int myview_text=0x7f0100a7;
....
}
....
public static final class styleable {
....
    public static final int[] MyView = {
            0x7f0100a7
    };
    public static final int MyView_myview_text = 0;
....
}

可以看出R.styleable.MyView是一个int型数组,里面的内容是与R.attr.myview_text相对应的.这就表明了R.style.MyView是一个存放了attrs.xml文件中声明的一组自定义属性的id集合.而R.styleable.MyView_myview_text则是一个属性在数组中对应的下标索引.
如果还是存在疑惑,可以在attrs.xmlMyView属性节点下多添加几个自定义的属性,然后重新编译代码,按照我上面的方法查看相应的代码,我相信你能够明白其中的关系.

我们可以点到context.obtainStyledAttributes(AttributeSet set, @StyleableRes int[] attrs)这个函数的源码中去,发现这个函数调用的是context.getTheme().obtainStyledAttributes(set, attrs, 0, 0)这个函数.可以看到这是一个4个参数的函数,它的函数原型如下:

/**
*
* @param set 需要解析的属性集合
* @param attrs 属性集合对应的id资源数组
* @param defStyleAttr 当前Theme中包含的一个指向style样式的引用.
*                     当我们没有设置自定义属性时,默认会从该集合中查找布局文件的属性配置值(0代表不向defStyleAttr查找属性默认值)
* @param defStyleRes 也是一个指向Style的资源ID
*                    当defStyleAttr==0 或 defStyleAttr!=0 但Theme中没有为defStyleAttr赋值,该参数才起作用.
* @return 
*/
public TypedArray obtainStyledAttributes(AttributeSet set,@StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)

代码中的注释已经说的很清楚,其中第3个参数和第4个参数分别与View的3参数的构造函数中的第3个参数和4参数构造函数中的第4个参数的意义是一样的.最后当我们使用完了TypeArray,我们需要将它回收,因为它是一个共享的资源.
由上面的函数就可以看出属性可以在很多的地方进行赋值,包括: XML布局文件中、decalare-styleable、theme中等,它们之间是有优先级次序的,按照优先级从高到低排序如下:

在布局xml中直接定义 > 在布局xml中通过style定义 > 自定义View所在的Activity的Theme中指定style引用 > 构造函数中defStyleRes指定的默认值

下面我们来尝试一下如何从第三个参数获取属性值.首先我们在attr.xml文件中添加一个单独的属性MyViewDefStyleAttr,格式为reference,就是资源引用类型.并在MyView属性组下添加一个属性.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyView">
        <attr name="myview_text" format="string"/>
        <attr name="myview_attr" format="string"/>
    </declare-styleable>
    <attr name="MyViewDefStyleAttr" format="reference"/>
</resources>

接下来我们修改style.xml文件,添加一个自定义的style,作为MyViewDefStyleAttr的实现,并在AppTheme 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="MyViewDefStyleAttr">@style/MyViewDefStyleaAttrImpl</item>
    </style>

    <style name="MyViewDefStyleaAttrImpl">
        <item name="myview_attr">attr</item>
    </style>

</resources>

然后我们就可以在第三个构造函数中获取自定义属性.

    /**
     * 在layout文件声明View时会调用,只有实现了这个构造函数才能在布局文件中声明此自定义View
     * @param context
     * @param attrs 存有View在xml布局文件中的自定义的属性
     */
    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs,R.attr.MyViewDefStyleAttr);

    }

    /**
     * 与第二个构造函数不同的地方在于,此构造函数可以指定View的默认样式
     * @param context
     * @param attrs
     * @param defStyleAttr 是当前theme中包含的指向View的样式资源的属性(0代表此参数无效)
     */
    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0);
        try {
            String s = a.getString(R.styleable.MyView_myview_attr);
            Log.e("RESULT",s);
        }finally {
            a.recycle();
        }

    }

这里我们通过两个参数的构造函数调用3参数的构造函数,并传入R.attr.MyViewDefStyleAttr作为默认样式属性值资源,在第三个构造函数中,若要获取第3个参数的默认属性值,必须通过显式的调用context.getTheme().obtainStyledAttributes(attrs,R.styleable.MyView,defStyleAttr, 0)这个4个参数的obtainStyledAttributes()函数.我们运行代码后得到的相应的结果:

《Android学习笔记---自定义View#01》

通过对View的构造函数的研究,基本了解了View在创建时是通过什么方式获取自定义属性的,并且也知道了该如何实现View的自定义属性.

参考

Android View构造函数详解

    原文作者:Kenny_Ho
    原文地址: https://www.jianshu.com/p/39f0640a063e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞