為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屬性。
假設我們有一個自定義的View,其類名是com.ispring.attr.MyView,其中com.ispring.attr是應用程式的包名。
要想自定義XML屬性,總的來說包括三步:
- 在xml資源檔案中定義各種
,指定attr
的資料類型。attr
- 在自定義View的構造函數中解析這些從XML中定義的屬性值,将其存放到View對應的成員變量中。
- 在layout檔案中為自定義View的XML屬性指派。
首先,我們在res/values目錄下建立了一個名為attrs_my_views.xml檔案,檔案名是什麼不重要,隻要是xml檔案就行。我們在該檔案中定義MyView所支援的XML屬性。該檔案的根結點是
<resources>
,我們在
<resources>
節點下可以添加多個
<attr>
節點,在
<attr>
節點中通過
name
指定XML屬性名稱,通過
format
指定XML屬性值的類型,如下圖所示:

由上圖我們可知,format支援的類型有enum、boolean、color、dimension、flag、float、fraction、integer、reference、string。
當我們指定了XML屬性的名稱和屬性值的類型之後,我們就可以在layout檔案中通過XML屬性為其指派了。如下圖所示:
我們通過
<com.ispring.attr.MyView>
在layout中引入了MyView,為了能夠使用自定義屬性,我們通常要指定一個自定義的命名空間以差別于Android的命名空間
xmlns:android
,我們自定義命名空間的名字可以是任意的,通常我一般用
xmlns:app
。我的App的命名空間是
com.ispring.attr
,如果用Eclipse開發,那麼可以這樣定義命名空間
xmlns:app=http://schemas.android.com/apk/res/com.ispring.attr
,但是在Android Studio中這樣定義命名空間會有問題。Android Studio使用Gradle進行build,而Gradle不允許自定義的命名空間以包名結尾,在Android Studio中可以這樣定義命名空間
xmlns:app="http://schemas.android.com/apk/res-auto"
,這樣定義的命名空間自動指向目前App的命名空間。
在正确定義app的命名空間之後,我們就可以用
app:customAttr
為MyView的customAttr屬性指派了。如果我們将customAttr的format定義為boolean的,那麼此處就隻能填寫true或者false,填寫其他類型的值會報錯。
下面再對attr的format的類型進行一下說明。
-
boolean
boolean表示attr是布爾類型的值,取值隻能是true或false。
-
string
string表示attr是字元串類型。
-
integer
integer表示attr是整數類型,取值隻能是整數,不能是浮點數。
-
float
float表示attr是浮點數類型,取值隻能是浮點數或整數。
-
fraction
fraction表示attr是百分數類型,取值隻能以%結尾,例如30%、120.5%等。
-
color
color表示attr是顔色類型,例如#ff0000,也可以使用一個指向Color的資源,比如@android:color/background_dark,但是不能用0xffff0000這樣的值。
-
dimension
dimension表示attr是尺寸類型,例如取值16px、16dp,也可以使用一個指向
類型的資源,比如<dimen>
。@android:dimen/app_icon_size
-
reference
reference表示attr的值隻能指向某一資源的ID,例如取值
。@id/textView
-
enum
enum表示attr是枚舉類型,在定義enum類型的attr時,可以将attr的format設定為enum,也可以不用設定attr的format屬性,但是必須在attr節點下面添加一個或多個enum節點。如下所示:
這樣attr的屬性值隻能取man或woman了。<attr name="customAttr"> <enum name="man" value="0" /> <enum name="woman" value="1" /> </attr>
-
flag
flag表示attr是bit位标記,flag與enum有相似之處,定義了flag的attr,在設定值時,可以通過
設定多個值,而且每個值都對應一個bit位,這樣通過按位或操作符|
可以将多個值合成一個值,我們一般在用flag表示某個字段支援多個特性,需要注意的是,要想使用flag類型,不能在attr上設定format為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>
節點下通過定義多個<attr>
表示其支援的值,value的值一般是0或者是2的N次方(N為大于等于0的整數),對于上面的例子我們在實際設定值是可以設定單獨的值,如none、bold、italic、underline,也可以通過<flag>
設定多個值,例如|
app:customAttr="italic|underline"
。
MyView直接繼承自View,我想讓MyView可以顯示文本,即我傳遞文本給MyView,MyView能畫出來,就相當于非常簡單的TextView。
是以,我的attrs_my_view.xml如下所示:
<resources>
<attr name="customText" format="string" />
<attr name="customColor" format="color" />
</resources>
我們定義了兩個XML屬性,customText是一個string類型,表示MyView要顯示的文本,customColor是color類型,表示文本的顔色。
對項目進行編譯之後會生成R.java檔案,R.java檔案對應着R類。如果是Android Studio項目,那麼R檔案的目錄是app\build\generated\source\r\debug\com\ispring\attr\R.java,在該檔案中有内部類
public static final class attr
,在R.attr中會發現有
customText
和
customColor
,如下圖所示:
R.attr.customText
和
R.attr.customColor
分别是屬性
customText
和
customColor
的資源ID。
在使用MyView時,我們可以在layout檔案中為MyView設定customText和customColor兩個XML屬性。layout檔案如下所示:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.ispring.attr.MainActivity">
<com.ispring.attr.MyView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:customText="Hello World!"
app:customColor="#FF0000FF"
/>
</RelativeLayout>
運作效果如下所示:
可以看出在界面上顯示了藍色的“Hello World!”文本,說明MyView的自定義屬性起作用了。
我們看一下MyView的具體實作,MyView的代碼如下所示:
package com.ispring.attr;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.View;
public class MyView extends View {
//存儲要顯示的文本
private String mCustomText;
//存儲文本的顯示顔色
private int mCustomColor = ;
//畫筆
private TextPaint mTextPaint;
//字型大小
private float fontSize = getResources().getDimension(R.dimen.fontSize);
public MyView(Context context) {
super(context);
init(null, );
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, );
}
public MyView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs, defStyle);
}
private void init(AttributeSet attrs, int defStyle) {
//首先判斷attrs是否為null
if(attrs != null){
//擷取AttributeSet中所有的XML屬性的數量
int count = attrs.getAttributeCount();
//周遊AttributeSet中的XML屬性
for(int i = ; i < count; i++){
//擷取attr的資源ID
int attrResId = attrs.getAttributeNameResource(i);
switch (attrResId){
case R.attr.customText:
//customText屬性
mCustomText = attrs.getAttributeValue(i);
break;
case R.attr.customColor:
//customColor屬性
//如果讀取不到對應的顔色值,那麼就用黑色作為預設顔色
mCustomColor = attrs.getAttributeIntValue(i, );
break;
}
}
}
//初始化畫筆
mTextPaint = new TextPaint();
mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextSize(fontSize);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mCustomText != null && !mCustomText.equals("")){
mTextPaint.setColor(mCustomColor);
//将文本繪制顯示出來
canvas.drawText(mCustomText, , fontSize, mTextPaint);
}
}
}
我們在MyView中定義了兩個成員變量mCustomText和mCustomColor。MyView的幾個構造函數都會調用init方法,我們重點看一下init方法。
- 傳遞給init方法的是一個AttributeSet對象,可以把它看成一個索引數組,這個數組裡面存儲着屬性的索引,通過索引可以得到XML屬性名和屬性值。
- 通過調用AttributeSet的getAttributeCount()方法可以獲得XML屬性的數量,然後我們就可以在for循環中通過索引周遊AttributeSet的屬性名和屬性值。AttributeSet中有很多getXXX方法,一般必須的參數都是索引号,說幾個常用的方法:
- 通過AttributeSet的
方法可以得到對應索引的XML屬性名。public abstract String getAttributeName (int index)
- 通過AttributeSet的
方法可以得到對應索引的XML屬性在R.attr中的資源ID,例如R.attr.customText、R.attr.customColor。public abstract int getAttributeNameResource (int index)
- 如果index對應的XML屬性的format是string,那麼通過AttributeSet的
方法,可以得到對應索引的XML屬性的值,該方法傳回的是String。除此之外,AttributeSet還有getAttributeIntValue、getAttributeFloatValue、getAttributeListValue等方法,傳回不同類型的屬性值。public abstract String getAttributeName (int index)
- 通過AttributeSet的
- 我們通過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。
- 我們重寫了MyView的onDraw方法,通過執行mTextPaint.setColor(mCustomColor)把畫筆設定成mCustomColor的顔色,通過執行canvas.drawText(mCustomText, 0, fontSize, mTextPaint)将mCustomText繪制到界面上。這樣,MyView就使用了customText和customColor這兩個XML屬性。
使用 <declare-styleable>
和obtainStyledAttributes方法
<declare-styleable>
我們上面定義的customText和customColor這兩個
<attr>
屬性都是直接在
<resources>
節點下定義的,這樣定義
<attr>
屬性存在一個問題:不能想通過style或theme設定這兩個屬性的值。
要想能夠通過style或theme設定XML屬性的值,需要在
<resources>
節點下添加
<declare-styleable>
節點,并在
<declare-styleable>
節點下定義
<attr>
,如下所示:
<resources>
<declare-styleable name="MyView">
<attr name="customText" format="string" />
<attr name="customColor" format="color" />
</declare-styleable>
</resources>
需要給
<declare-styleable>
設定name屬性,一般name設定為自定義View的名字,我們此處設定為MyView。
在
<declare-styleable>
下面定義的
<attr>
屬性與直接在
<resources>
定義的
<attr>
屬性其實本質上沒有太大差別,無論哪種方式定義
<attr>
,都會在R.attr類中定義R.attr.customText和R.attr.customColor。不同的是,
<declare-styleable name="MyView">
節點會在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.MyView的作用會在下面介紹。
我們同樣可以在R.styleable中發現R.styleable.MyView_customColor和R.styleable.MyView_customText這兩個ID。
<declare-styleable name="MyView">
中的name加上裡面的
<attr>
屬性的name就組成了R.styleable中的MyView_customColor和MyView_customText,中間以下劃線連接配接。如下圖所示:
其中R.styleable.MyView_customColor對應R.attr.customColor,R.styleable.MyView_customText對應R.attr.customText。MyView_customColor和MyView_customText的作用在下面介紹。
在
<declare-styleable>
中定義的
<attr>
在MyView中需要通過調用theme的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)
我們先看第三個最複雜的方法,在MyView中使用方法如下所示:
private void init(AttributeSet attributeSet, int defStyle) {
//首先判斷attributeSet是否為null
if(attributeSet != null){
//擷取目前MyView所在的Activity的theme
Resources.Theme theme = getContext().getTheme();
//通過theme的obtainStyledAttributes方法擷取TypedArray對象
TypedArray typedArray = theme.obtainStyledAttributes(attributeSet, R.styleable.MyView, , );
//擷取typedArray的長度
int count = typedArray.getIndexCount();
//通過for循環周遊typedArray
for(int i = ; i < count; i++){
//通過typedArray的getIndex方法擷取指向R.styleable中對應的屬性ID
int styledAttr = typedArray.getIndex(i);
switch (styledAttr){
case R.styleable.MyView_customText:
//如果是R.styleable.MyView_customText,表示屬性是customText
//通過typedArray的getString方法擷取字元串值
mCustomText = typedArray.getString(i);
break;
case R.styleable.MyView_customColor:
//如果是R.styleable.MyView_customColor,表示屬性是customColor
//通過typedArray的getColor方法擷取整數類型的顔色值
mCustomColor = typedArray.getColor(i, );
break;
}
}
//在使用完typedArray之後,要調用recycle方法回收資源
typedArray.recycle();
}
...
}
我們在res/valeus/styles.xml檔案中定義了如下style:
<style name="RedStyle">
<item name="customText">customText in RedStyle</item>
<!-- 紅色 -->
<item name="customColor">#FFFF0000</item>
</style>
然後我們在layout檔案中将MyView的style屬性設定為上面的style,如下所示:
<com.ispring.attr.MyView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:customText="customText in AttributeSet"
style="@style/RedStyle"
/>
運作效果如下所示:
我們雖然沒有直接設定MyView的customText和customColor兩個屬性,但是通過設定style屬性之後,在效果上RedStyle中所定義的屬性值應用到了MyView上了。
下面我們就來解釋一下init方法:
- 首先我們通過
擷取到了目前MyView所在的Activity的theme。getContext().getTheme()
- 然後調用方法
,該方法傳回一個TypedArray對象。TypedArray是一個數組,通過該數組可以擷取應用了style和theme的XML屬性值。上面這個方法有四個參數,後面兩個參數都是0,大家暫且忽略不計,後面會介紹。第一個參數還是AttributeSet對象,第二個參數是一個int類型的數組,該數組表示想要擷取的屬性值的屬性的R.attr中的ID,此處我們傳入的是R.styleable.MyView,在上面我們已經提到其值等價于[R.attr.customText, R.attr.customColor],表示我們此處想擷取customText和customColor這兩個屬性的值。theme.obtainStyledAttributes(attributeSet, R.styleable.MyView, 0, 0)
- 如果在layout檔案中直接為MyView設定了某些XML屬性,那麼這些XML屬性及其屬性值就會出現在AttributeSet中,那麼Android就會直接使用AttributeSet中該XML屬性值作為
的傳回值,比如在上面的例子中,我們通過theme.obtainStyledAttributes()
設定了MyView的XML屬性,最終運作的效果顯示的也是文本”customText in AttributeSet”。app:customText="customText in AttributeSet"
- 如果在layout檔案中沒有為MyView設定某個XML屬性,但是給MyView設定了style屬性,例如
,并且在style中指定了相應的XML屬性,那麼Android就會用style屬性所對應的style資源中的XML屬性值作為style="@style/RedStyle"
的傳回值。比如在上面的例子中,我們在layout檔案中沒有設定theme.obtainStyledAttributes()
的值,但是在其style屬性所對應的app:customColor
資源中将RedStyle
設定成了紅色customColor
,最終文本也是以紅色顯示在界面上的。#FFFF0000
通過以上描述,我們可以知道,View的style屬性對應的style資源中定義的XML屬性值其實是View直接在layou檔案中定義XML屬性值的替補值,是用于補漏的,AttributeSet(即在layout中直接定義XML屬性)的優先級高于style屬性中資源所定義的屬性值。
obtainStyledAttributes方法之defStyleAttr
我們再看一下方法
obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
中的第三個參數defStyleAttr,這個參數表示的是一個
<style>
中某個屬性的ID,當Android在AttributeSet和style屬性所定義的style資源中都沒有找到XML屬性值時,就會嘗試查找目前theme(theme其實就是一個
<style>
資源)中屬性為defStyleAttr的值,如果其值是一個style資源,那麼Android就會去該資源中再去查找XML屬性值。可能聽起來比較費勁,我們舉個例子。
我們更改了values/styles.xml檔案,如下所示:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="myViewStyle">@style/GreenStyle</item>
</style>
<style name="GreenStyle">
<item name="customText">customText in GreenStyle</item>
<item name="customColor">#FF00FF00</item>
</style>
<style name="HelloWorldStyle">
<item name="customText">Hello World!</item>
</style>
<style name="RedStyle">
<item name="customText">customText in RedStyle</item>
<item name="customColor">#FFFF0000</item>
</style>
</resources>
我們添加了HelloWorldStyle和GreenStyle,其中HelloWorldStyle隻定義了customText的屬性值,而GreenStyle同時定義了customText和customColor的值,其中customColor定義為綠色。
在AppTheme中,我們設定了myViewStyle這個屬性的值,如下所示:
<item name="myViewStyle">@style/GreenStyle</item>
myViewStyle這個屬性是在values/attrs_my_view.xml中定義的,如下所示:
<resources>
<attr name="myViewStyle" format="reference" />
<declare-styleable name="MyView">
<attr name="customText" format="string" />
<attr name="customColor" format="color" />
</declare-styleable>
</resources>
myViewStyle被定義為一個reference格式,即其值指向一個資源類型,我們在AppTheme中将其指派為
@style/GreenStyle
,即在AppTheme中,myViewStyle的就是GreenStyle,其指向了一個style資源。
我們更改MyView代碼,如下所示:
//通過theme的obtainStyledAttributes方法擷取TypedArray對象
TypedArray typedArray = theme.obtainStyledAttributes(attributeSet, R.styleable.MyView, R.attr.myViewStyle, );
注意,上面obtainStyledAttributes方法最後一個參數還是為0,可以忽略,但是第三個參數的值不再是0,而是R.attr.myViewStyle。
然後我們更新layout檔案,如下所示:
<com.ispring.attr.MyView
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/HelloWorldStyle"
/>
我們為MyView設定了style屬性,其值為HelloWorldStyle。
程式運作效果如下所示:
我們發現結果是綠色的“Hello World!”,我們解釋一下原因。
- 由于這次我們沒有通過layout檔案直接設定MyView的XML屬性的值,是以AttributeSet本身是沒有XML屬性值的,我們直接忽略掉AttributeSet。
- 我們通過
為MyView設定了style為HelloWorldStyle,HelloWorldStyle中定義customText的屬性值為”Hello World!”,是以最終customText的值就是”Hello World!”,在界面上顯示的也是該值。style="@style/HelloWorldStyle"
- HelloWorldStyle中并沒有定義customColor屬性值。我們将theme.obtainStyledAttributes()方法的第三個參數設定為R.attr.myViewStyle,此處的theme就是我們上面提到的AppTheme,Android會去AppTheme中查找屬性為myViewStyle的值,我們之前提到了,它的值就是
,即GreenStyle,由于該值是個style資源,Android就會去該資源中查找customColor的值,GreenStyle定義了customColor的顔色為綠色,是以MyView最終所使用的customColor的值就是綠色。@style/GreenStyle
綜上,我們發現,此處的第三個參數的作用是:當在AttributeSet和style屬性中都沒有找到屬性值時,就去Theme的某個屬性(即第三個參數)中檢視其值是否是style資源,如果是style資源再去這個style資源中查找XML屬性值作為替補值。
obtainStyledAttributes方法之defStyleRes
最後我們看一下方法
obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
中的第四個參數defStyleRes。與defStyleAttr類似,defStyleRes是前面幾項的替補值,defStyleRes的優先級最低。與defStyleAttr不同的是,defStyleRes本身直接表示一個style資源,而theme要通過屬性defStyleAttr間接找到style資源。
我們添加了BlueStyle這個style,如下所示:
<style name="BlueStyle">
<item name="customText">customText in BlueStyle</item>
<item name="customColor">#FF0000FF</item>
</style>
并将layout檔案改為如下所示:
<com.ispring.attr.MyView
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
我們更改MyView代碼如下所示:
第三個參數設定為0,第四個參數不再是0,而是R.style.BlueStyle。運作界面如下所示:
隻有第三個參數defStyleAttr為0或者該屬性在theme中找不到時,才會使用第四個參數defStyleRes。如果第三個參數defStyleAttr不為0,但是theme的defStyleAttr所對應的屬性值中的style沒有定義任何XML屬性值,那麼第四個參數也不會defStyleRes被使用。
總結
- 可以不通過
節點定義XML屬性,不過還是建議将XML屬性定義在<declare-styleable>
節點下,因為這樣Android會在R.styleable下面幫我們生成很多有用的常量供我們直接使用。<declare-styleable>
- obtainStyledAttributes方法中,優先級從高到低依次是:直接在layout中設定View的XML屬性值(AttributeSet) > 設定View的style屬性 > defStyleAttr > defStyleRes
希望本文對大家了解與使用自定義XML屬性有所幫助!
相關閱讀:
我的Android博文整理彙總