深入了解Android自定义属性

自定义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下了,然后在需要使用时,自己构建数组引用即可。
本文就到这里,谢谢各位。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容