深入了解Android自定义属性

作者: Clement_wu | 来源:发表于2017-06-19 22:48 被阅读230次

    自定义view的时候,有时需要用到自定义属性,方便我们定制View。一般来说,自定义属性过程如下:

    1. 定义属性:在values下的attrs.xml内编写declare-styleable标签来定义属性;
    2. 使用属性:在布局文件中通过获取
    3. 获取属性:在自定义view中使用TypedArray获取自定义的属性。

    下面按照自定义View的流程讲讲自定义属性:
    首先说明一下自定义View的四个构造函数的区别:

    public class CustomView extends View {
        public CustomView(Context context) {
            super(context);
        }
    
        public CustomView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
        }
    }
    

    在使用过程中,一般都是联级调用。第一个构造函数用处并不大,主要在Java代码中声明View时才使用;在布局文件中使用自定义的View,则调用的是第二个构造函数;第三,第四个构造函数是与系统主题有关的,从参数defStyleAttrdefStyleRes也可以看得出来;也就是说,在自定义View时,如果不需要view跟随主题改变,则前两个构造函数就足够了。

    关于AttributeSet

    在第二个构造函数中,有个AttributeSet参数,那这个参数有啥用的呢?
    查看源码,我们可以发现AttributeSet是个接口,该接口用于解析xml布局中的属性。举个例子,假设定义了一个CustomerView,在布局中使用:

    <com.example.developmc.customview.CustomView
            android:layout_width="100dp"
            android:layout_height="100dp" />
    

    然后我们在构造函数中用如下代码打印AttributeSet中的属性:

    public CustomView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            int attributeCount = attrs.getAttributeCount();
            for (int i = 0; i < attributeCount; i++) {
                String attrName = attrs.getAttributeName(i);
                String attrVal = attrs.getAttributeValue(i);
                Log.e("AttributeSet:", "attrName = " + attrName + " , attrVal = " + attrVal);
            }
        }
    

    打印结果如下:

    attrName = layout_width , attrVal = 100.0dip
    attrName = layout_height , attrVal = 100.0dip
    

    可以看到AttributeSet包含了所有在布局中定义的属性,并且能够按顺序地取得各个属性的name和value。换言之,AttributeSet用于解析View在xml布局中的所有属性的name和value,这也是自定义View要使用第二个构造函数的原因。

    细心观察一下,我们在布局中layout_width的值是100dp,但打印出来的值却是100dip,这单位不对呀!抱着疑问,我们修改一下布局:

    <com.example.developmc.customview.CustomView
            android:layout_width="@dimen/dimen_100"
            android:layout_height="100dp" />
    

    其中dimen_100的定义是:

    <dimen name="dimen_100">100dp</dimen>
    

    再次打印,结果如下:

    attrName = layout_width , attrVal = @2131165262
    attrName = layout_height , attrVal = 100.0dip
    

    可以看到layout_width变成了一个奇怪的值@2131165262,这个值是怎么来的呢?
    我们知道,Android会在R.java中为定义的属性生成资源标识符(一个十六进制的数值)。在app/build/generated/r/debug下找到并打开R.java,找到dimen_100,对应的值是0x7f07004e,将这个数值转为十进制,正好就是“2131165262”!

    到这里,我们可以得出结论,当在布局中直接赋值(如:android:layout_width="100dp"),Attribute拿到的值,数值是正确的,但单位可能会不对; 当在布局中为属性赋引用值(如:android:layout_width="@dimen/dimen_100"),Attribute拿到的是该值对应的资源标识符!总的来说,Attribute解析出来的属性值并不能直接使用!不要怕,TypedArray就是用于简化这方面的工作的。

    关于TypedArray

    先来回忆一下,我们是如何同时使用Arrtibute和TypedArray的:
    首先,新建文件attrs.xml,为自定义View添加一个自定义属性:

    <declare-styleable name="CustomView">
            <attr name="customWidth" format="dimension"/>
        </declare-styleable>
    

    然后在布局文件中使用自定义的属性customWidth

    <com.example.developmc.customview.CustomView
            android:layout_width="@dimen/dimen_100"
            android:layout_height="100dp"
            app:customWidth="@dimen/dimen_100"/>
    

    最后在构造函数中用TypedArray解析customWidth的value,代码如下:

    public CustomView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            int attributeCount = attrs.getAttributeCount();
            for (int i = 0; i < attributeCount; i++) {
                String attrName = attrs.getAttributeName(i);
                String attrVal = attrs.getAttributeValue(i);
                Log.e("AttributeSet:", "attrName = " + attrName + " , attrVal = " + attrVal);
            }
            TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.CustomView);
            float width = array.getDimension(R.styleable.CustomView_customWidth,200f);
            Log.e("width:", "widthVal = "+String.valueOf(width));
            array.recycle();
        }
    

    打印结果是:

    attrName = layout_width , attrVal = @2131165262
    attrName = layout_height , attrVal = 100.0dip
    attrName = customWidth , attrVal = @2131165262
    widthVal = 350.0
    

    测试虚拟机的像素是:560dpi,那么通过计算,100dp对应的像素就是350dip
    通过打印结果,可以看到使用Attribute和TypedArray的区别:如果使用Attribute,拿到的结果并不能直接使用,需要进一步处理;而TypedArray则直接取得了正确的数值,简化了这个步骤。所以在使用自定义属性时,我们总是应该使用TypedArray的方式获取属性值。
    这里需要注意的一点是:每次使用完TypedArray之后,要记得调用recycle()回收。这是为什么呢?
    从上述代码我们是通过context.obtainStyledAttributes获取TypeadArray实例的,并不是通过new实例的方式获取的。事实上,TypedArray类,没有公有的构造函数,是一个典型的单例模式,程序在运行时维护一个TypedArray池,使用时,向该池中请求一个实例,用完之后,调用 recycle() 方法来释放该实例,从而使其可被其他模块复用。

    关于declare-styleable

    我们一般在attrs.xml中通过<declare-styleable>标签声明自定义的属性;先看看下面的代码:

    <resources>
        <declare-styleable name="CustomView">
            <attr name="customWidth" format="dimension"/>
        </declare-styleable>
        <attr name="customHeight" format="dimension"/>
    </resources>
    

    在上述attrs.xml中,我们自定义了两个属性:customWidth和customHeight,其中customHeight没有声明在declare-styleable。运行代码后,在生成的R.java文件中,可以同时看到这两个属性:

    public static final class attr {
        public static final int customWidth=0x7f0100ce;
        public static final int customHeight=0x7f010001;
    }
    

    可以看到定义在declare-styleable中与直接用attr定义没有实质的不同,系统都会为我们在R.attr中生成响应的属性。但不同的是,如果声明在declare-styleable中,系统除了在R.java的attr类下生成资源标识符,还会为我们在R.java内的styleable类中生成相关的属性:

    public static final class styleable {
          public static final int[] CustomView = {
                0x7f0100ce
            };
    }
    

    可以看到customWidth属性对应的标识符0x7f0100ce被保存在styleable内的数组中。如上所示,R.styleable.CustomView是一个int[],而里面的元素正是declare-styleable内声明的元素,这个数组在自定义View的构造函数中获得属性值时会用到。
    将自定义属性分组声明在declare-styleabe中的作用就是系统会自动为我们生成这些东西,如果不声明在declare-styleable中,我们也可以在需要的时候自己构建这个数组,只是比较麻烦。当我们有多个自定义View需要用到同一个自定义属性时,就不能同时在两个declare-styleabe下声明同一个属性了(编译不通过),这时就可以把这个属性直接定义在attr下了,然后在需要使用时,自己构建数组引用即可。
    本文就到这里,谢谢各位。

    相关文章

      网友评论

        本文标题:深入了解Android自定义属性

        本文链接:https://www.haomeiwen.com/subject/diaafxtx.html