前言
回顧一下:自定義View的時候乒融,根據(jù)不同條件設(shè)置不同顏色详民,那么需要提供對外的方法設(shè)置顏色吁伺。而View可以在xml里引用逞带,我們想當(dāng)然的認(rèn)為是否能夠在xml里根據(jù)不同條件設(shè)置顏色屬性呢欺矫?這樣的話就很靈活了。當(dāng)然展氓,Android系統(tǒng)已經(jīng)為我們準(zhǔn)備好了穆趴,接下來我們來分析其解析原理及其使用。
通過本篇文章遇汞,你將了解到:
1未妹、自定義屬性基礎(chǔ)明晰
2簿废、自定義屬性使用
3、attr/style/theme聯(lián)系與區(qū)別
4络它、自定義屬性加載優(yōu)先級
5族檬、自定義屬性加載源碼分析
自定義屬性基礎(chǔ)明晰
attrs.xml
注 為表述方便,以下的"屬性聲明" 指的是該屬性聲明了但沒有賦值化戳;而"屬性定義”指的是該屬性被使用单料,也即被賦值了。
在res->values目錄下新建attrs.xml文件点楼,該文件用來聲明屬性名及其接受的數(shù)據(jù)格式:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="attr_str" format="string"></attr>
<attr name="attr_bool" format="boolean"></attr>
<attr name="attr_int" format="integer"></attr>
<attr name="attr_ref" format="reference"></attr>
</resources>
其中 name表示屬性名扫尖,format表示其接受的輸入格式。
以上聲明了三個屬性掠廓,分別代表string换怖、boolean、integer却盘、reference格式狰域。reference指向其它資源。
format還有其它格式黄橘,如:
color -- 顏色值
dimension -- 尺寸
float -- 浮點值
fraction -- 百分比
enum -- 枚舉
flag -- 位或運算
混合類型 -- 多種format結(jié)合
enum和flag聲明(可以不指定format) 兆览,如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="attr_enum">
<enum name="first" value="1"></enum>
<enum name="second" value="2"></enum>
<enum name="third" value="3"></enum>
</attr>
<attr name="attr_flag">
<flag name="east" value="0x1"></flag>
<flag name="west" value="0x2"></flag>
<flag name="south" value="0x3"></flag>
<flag name="north" value="0x4"></flag>
</attr>
</resources>
自定義屬性使用
原始使用方式
public class MyAttrView extends View {
private final String TAG = MyAttrView.class.getSimpleName();
public MyAttrView(Context context) {
super(context);
}
public MyAttrView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
for (int i = 0; i < attrs.getAttributeCount(); i++) {
Log.d(TAG, "name:" + attrs.getAttributeName(i) + " value:" + attrs.getAttributeValue(i));
}
}
}
定義View: MyAttrView,并在布局xml里引用此View:
<com.fish.myapplication.attr.MyAttrView
app:attr_str="hello world str"
app:attr_bool="true"
app:attr_int="99"
app:attr_ref="@dimen/dp_100"
android:layout_width="100px"
android:layout_height="100px">
</com.fish.myapplication.attr.MyAttrView>
在布局xml里引用了在attrs.xml聲明的4個屬性塞关,并在MyAttrView里解析抬探。
AttributeSet是個xml解析工具類,幫助我們從布局的xml里提取屬性名和屬性值帆赢,它是個接口小压,實現(xiàn)類是:XmlBlock里的子類Parser。
將屬性名和屬性值打印椰于,結(jié)果如下:
可以看出怠益,AttributeSet將布局xml下的屬性全部打印出來了,但是有兩個問題:
1瘾婿、attr_ref屬性想要的是一個整數(shù)尺寸蜻牢,卻返回了資源編號。
2偏陪、layout_width/layout_height 我們并不關(guān)心此屬性抢呆,我們只關(guān)心自定義屬性。如果能傳入關(guān)心的屬性集合笛谦,并返回其值抱虐,那最好不過了。
有沒有一種方式能夠解決上述兩種問題呢饥脑?答案是:TypedArray恳邀。
進階使用方式(TypedArray)
依舊在attrs.xml里改造:
<resources>
<declare-styleable name="MyStyleable">
<attr name="attr_str" format="string"></attr>
<attr name="attr_bool" format="boolean"></attr>
<attr name="attr_int" format="integer"></attr>
<attr name="attr_ref" format="reference"></attr>
</declare-styleable>
</resources>
相比我們剛開始聲明的屬性而言懦冰,增加了“declare-styleable”標(biāo)簽,意思是將若干個屬性聲明歸結(jié)到MyStyleable里轩娶,這些屬性聲明屬于"同一組"儿奶。
再來看看如何解析這些屬性。
public MyAttrView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//R.styleable.MyStyleable 指的是想要解析的屬性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyStyleable);
//count 表示解析出來的個數(shù)
int count = typedArray.getIndexCount();
for (int i = 0; i < count; i++) {
int indexValue = typedArray.getIndex(i);
//通過屬性index找到屬性值
switch (indexValue) {
case R.styleable.MyStyleable_attr_str:
String strValue = typedArray.getString(indexValue);
Log.d(TAG, "str value:" + strValue);
break;
case R.styleable.MyStyleable_attr_bool:
boolean boolValue = typedArray.getBoolean(indexValue, false);
Log.d(TAG, "bool value:" + boolValue);
break;
case R.styleable.MyStyleable_attr_int:
int intValue = typedArray.getInt(indexValue, 0);
Log.d(TAG, "int value:" + intValue);
break;
case R.styleable.MyStyleable_attr_ref:
float refValue = typedArray.getDimension(indexValue, 0);
Log.d(TAG, "float value:" + refValue);
break;
}
}
//typedArray 存放在緩存池鳄抒,因此用完歸還到緩存池
typedArray.recycle();
}
運行效果如下:
看得出來闯捎,尺寸的結(jié)果已經(jīng)轉(zhuǎn)換為實際值了。
重點方法如下:
context.obtainStyledAttributes(attrs, R.styleable.MyStyleable)
public final TypedArray obtainStyledAttributes(
@Nullable AttributeSet set, @NonNull @StyleableRes int[] attrs) {
return getTheme().obtainStyledAttributes(set, attrs, 0, 0);
}
有兩個參數(shù):
AttributeSet set :當(dāng)前xml聲明的屬性集合
int[] attrs :想要取得屬性值的屬性名集合
可以看出R.styleable.MyStyleable實際上就是個整形數(shù)組许溅。與res目錄下的其它資源類似瓤鼻,其索引在編譯時期生成在R.java里。
數(shù)組里的元素值就是MyStyleable聲明里的屬性索引贤重,同樣的在R.java里找到其索引:
可以看出茬祷,R.styleable.MyStyleable就是我們想要解析的屬性名集合。
AttributeSet set 與 int attrs[]關(guān)系:
obtainStyledAttributes 方法返回值類型:TypedArray并蝗。該類型記錄了獲取到的屬性值集合(記錄在數(shù)組里)祭犯,而通過數(shù)組下標(biāo)索引即可找到對應(yīng)的屬性值。索引下標(biāo)通過R.styleable.MyStyleable_xx獲取滚停,"xx"表示屬性名沃粗,一般命名為"styleable名" + "_" + "屬性名"。同樣的键畴,這些值也記錄在R.java里:
在R.java里
MyStyleable_attr_bool 代表數(shù)組索引下標(biāo)
MyStyleable 代表屬性數(shù)組
attr_bool 代表屬性
總結(jié)來說:通過下標(biāo)取數(shù)組里屬性
綜上所述最盅,TypedArray很好地解決了我們在使用"原始方式"獲取屬性遇到的問題。
attr/style/theme聯(lián)系與區(qū)別
style 由來與作用
在res->values目錄下起惕,找到styles.xml文件(沒有則新建):
<resources>
<style name="myStyle">
<item name="attr_str">str in myStyle</item>
<item name="attr_bool">true</item>
</style>
</resources>
可以看出style批量定義了一批屬性涡贱。這樣做的好處顯而易見:利于復(fù)用屬性集合。
比如我們自定義的MyAttrView作為公共控件使用:
<com.fish.myapplication.attr.MyAttrView
app:attr_str="hello world str"
app:attr_bool="true"
android:layout_width="100px"
android:layout_height="100px">
</com.fish.myapplication.attr.MyAttrView>
使用的屬性值都是一樣的惹想,那么可以將這些屬性提取出來作為一個style項问词,在引用的時候引用style即可,不用到處重復(fù)定義屬性嘀粱。
<com.fish.myapplication.attr.MyAttrView
style="@style/myStyle"
android:layout_width="100px"
android:layout_height="100px">
</com.fish.myapplication.attr.MyAttrView>
theme 由來與作用
在res->values目錄下激挪,找到themes.xml文件(沒有則新建)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="myTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="attr_str">str in myTheme</item>
<item name="attr_bool">true</item>
</style>
</resources>
theme實際上也是用了style語法,parent表示其父類主題草穆,子類繼承父類屬性灌灾。theme如何使用呢搓译?
之前說過style為了View之間復(fù)用屬性集悲柱,那么theme是為了Activity/Application復(fù)用屬性集。因此些己,我們將theme配置給Activity或者Application豌鸡。
可以看出引用theme是通過style引用的嘿般,因此我們可以直接將style條目作為theme使用,只是一般為了直觀定義了themes.xml文件涯冠,該文件里的style作為theme使用炉奴。
總結(jié)來說三者關(guān)系:
style 是定義屬性的集合,使用style標(biāo)簽蛇更,作用于View
theme 是定義屬性的集合瞻赶,使用style標(biāo)簽,作用于Application/Activity
declare-styleable 是聲明屬性的集合派任,使用declare-styleable標(biāo)簽
自定義屬性加載優(yōu)先級
通過上述分析砸逊,定義屬性的方式目前看來有以下3種:
1、在布局文件里定義屬性
2掌逛、在style里定義屬性
3师逸、在theme里定義屬性
再重新來看看obtainStyledAttributes(xx)方法:
public final TypedArray obtainStyledAttributes(
@Nullable AttributeSet set, @NonNull @StyleableRes int[] attrs) {
return getTheme().obtainStyledAttributes(set, attrs, 0, 0);
}
public TypedArray obtainStyledAttributes(@Nullable AttributeSet set,
@NonNull @StyleableRes int[] attrs, @AttrRes int defStyleAttr,
@StyleRes int defStyleRes) {
return mThemeImpl.obtainStyledAttributes(this, set, attrs, defStyleAttr, defStyleRes);
}
第二個方法有4個參數(shù),前面兩個前面分析過豆混,來看看后邊兩個:
@AttrRes int defStyleAttr 形參限制為AttrRes篓像,說明是屬性類型
@StyleRes int defStyleRes 形參限制為StyleRes,說明是style類型
從形參名字來看皿伺,顯然是默認(rèn)的屬性與默認(rèn)的style员辩。
由此看出,obtainStyledAttributes(xx)方法負(fù)責(zé)解析了來自5個地方的屬性:
1心傀、在布局文件里定義屬性
2屈暗、在style里定義屬性
3、在theme里定義屬性
4脂男、默認(rèn)的屬性
5养叛、默認(rèn)的style
問題來了:如果上述5個來處都定義了同一個屬性,那么該以哪個屬性值為準(zhǔn)呢宰翅?在真相尚未揭開之前弃甥,用最基本的方法,一一測試來看規(guī)律汁讼。
首先先將各個屬性來處定義淆攻,以"attr_str"屬性為例:
1、在布局里定義屬性并使用:
#layout.xml 定義并使用
<com.fish.myapplication.attr.MyAttrView
app:attr_str="str in myLayout"
android:layout_width="100px"
android:layout_height="100px">
</com.fish.myapplication.attr.MyAttrView>
2嘿架、在style定義屬性并使用:
#styles.xml 定義
<style name="myStyle">
<item name="attr_str">str in myStyle</item>
</style>
#layout.xml 里使用style
<com.fish.myapplication.attr.MyAttrView
style="@style/myStyle"
android:layout_width="100px"
android:layout_height="100px">
</com.fish.myapplication.attr.MyAttrView>
3瓶珊、使用默認(rèn)屬性:
#themes.xml定義
#attr_ref 是引用類型的屬性,這里指向style
<style name="myTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="attr_ref">@style/myDefaultAttr</item>
</style>
#styles.xml
<style name="myDefaultAttr">
<item name="attr_str">str in myDefaultAttr</item>
</style>
#MyAttrView.java 里解析 傳入R.attr.attr_ref耸彪,最終找到myDefaultAttr里的attr_str屬性
context.obtainStyledAttributes(attrs, R.styleable.MyStyleable, R.attr.attr_ref, 0);
4伞芹、使用默認(rèn)style:
#在styles.xml里定義
<style name="myDefaultStyle">
<item name="attr_str">str in myDefaultStyle</item>
</style>
#MyAttrView.java 里解析 傳入R.style.myDefaultStyle,最終找到myDefaultStyle里的attr_str屬性
context.obtainStyledAttributes(attrs, R.styleable.MyStyleable, 0, R.style.myDefaultStyle);
5、使用theme里定義的屬性:
#themes.xml里定義
<style name="myTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="attr_str">str in myTheme</item>
</style>
context.obtainStyledAttributes(attrs, R.styleable.MyStyleable, 0, 0);
為了區(qū)分屬性值取自哪唱较,我們在不同的地方打印了相應(yīng)的關(guān)鍵字扎唾。上面定義了
1~ 5個不同來處的屬性,現(xiàn)在倒序從5 ~ 1依次添加這些屬性定義南缓。使用TypedArray解析出屬性值:
public MyAttrView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//R.styleable.MyStyleable 指的是想要解析的屬性
TypedArray typedArray = context.obtainStyledAttributes(attrs, null, 0, R.style.myDefaultStyle);
//count 表示解析出來的個數(shù)
int count = typedArray.getIndexCount();
for (int i = 0; i < count; i++) {
int indexValue = typedArray.getIndex(i);
//通過屬性index找到屬性值
switch (indexValue) {
case R.styleable.MyStyleable_attr_str:
String strValue = typedArray.getString(indexValue);
Log.d(TAG, "str value:" + strValue);
break;
}
}
typedArray.recycle();
}
五次運行結(jié)果如下:
我們依次添加的屬性定義胸遇,后面添加的將前面覆蓋了,說明后面添加的優(yōu)先級更高汉形,因此總結(jié)來說纸镊,自定義屬性優(yōu)先級自高到低是:
1、在布局文件里定義屬性
2概疆、在style里定義屬性
3薄腻、在theme里定義屬性
4、默認(rèn)的屬性
5届案、默認(rèn)的style
自定義屬性加載源碼分析
雖然以上通過測試說明了屬性是如何解析及其解析的優(yōu)先級庵楷,但是為了更好地理解其實際運作過程,我們需要分析源碼楣颠。從TypedArray和obtainStyledAttributes(xx)方法入手尽纽。
來看看obtainStyledAttributes(xx)調(diào)用流程:
applyStyle調(diào)用了native方法:
nativeApplyStyle(mObject, themePtr, defStyleAttr, defStyleRes,
parser != null ? parser.mParseState : 0, inAttrs, outValuesAddress,
outIndicesAddress);
注意到最后兩個參數(shù),分別對應(yīng)TypedArray兩個參數(shù):
outValuesAddress --> int[] mData;
outIndicesAddress --> int[] mIndices
最終調(diào)用了AttributeResolution.cpp 的ApplyStyle(xx)方法:
void ApplyStyle(Theme* theme, ResXMLParser* xml_parser, uint32_t def_style_attr,
uint32_t def_style_resid, const uint32_t* attrs, size_t attrs_length,
uint32_t* out_values, uint32_t* out_indices) {
//省略...
int indices_idx = 0;
uint32_t def_style_flags = 0u;
//如果傳入了默認(rèn)屬性
if (def_style_attr != 0) {
Res_value value;
//加載默認(rèn)屬性
if (theme->GetAttribute(def_style_attr, &value, &def_style_flags) != kInvalidCookie) {
if (value.dataType == Res_value::TYPE_REFERENCE) {
//并將值賦值給默認(rèn)style童漩,可以看出默認(rèn)屬性優(yōu)先級高于默認(rèn)style
def_style_resid = value.data;
}
}
}
//遍歷屬性名集合弄贿,也就是declare-styleable 聲明的屬性集合
for (size_t ii = 0; ii < attrs_length; ii++) {
//1、先加載XM里定義的屬性
if (xml_attr_idx != xml_attr_finder.end()) {
// We found the attribute we were looking for.
xml_parser->getAttributeValue(xml_attr_idx, &value);
}
if (value.dataType == Res_value::TYPE_NULL && value.data != Res_value::DATA_NULL_EMPTY) {
//2矫膨、上一步如果沒找到差凹,繼續(xù)在xml里的style里找
if (entry != xml_style_attr_finder.end()) {
// We found the attribute we were looking for.
cookie = entry->cookie;
type_set_flags = style_flags;
value = entry->value;
value_source_resid = entry->style;
}
}
if (value.dataType == Res_value::TYPE_NULL && value.data != Res_value::DATA_NULL_EMPTY) {
//3、還是沒找到侧馅,找默認(rèn)style
//這里需要注意的是危尿,上面用默認(rèn)attr賦值給默認(rèn)style,因此如果attr不為空馁痴,那么先加載了attr
//如果為空谊娇,那么加載默認(rèn)style
if (entry != def_style_attr_finder.end()) {
cookie = entry->cookie;
type_set_flags = def_style_flags;
value = entry->value;
}
}
if (value.dataType != Res_value::TYPE_NULL) {
//省略
} else if (value.data != Res_value::DATA_NULL_EMPTY) {
ApkAssetsCookie new_cookie = theme->GetAttribute(cur_ident, &value, &type_set_flags);
if (new_cookie != kInvalidCookie) {
//4、前面步驟都找不到罗晕,最后嘗試加載theme里的屬性
new_cookie =
assetmanager->ResolveReference(new_cookie, &value, &config, &type_set_flags, &resid);
if (new_cookie != kInvalidCookie) {
cookie = new_cookie;
}
}
}
//out_values存放類型济欢、屬性值,資源id小渊,密度等
out_values[STYLE_TYPE] = value.dataType;
out_values[STYLE_DATA] = value.data;
out_values[STYLE_ASSET_COOKIE] = ApkAssetsCookieToJavaCookie(cookie);
out_values[STYLE_RESOURCE_ID] = resid;
out_values[STYLE_CHANGING_CONFIGURATIONS] = type_set_flags;
out_values[STYLE_DENSITY] = config.density;
out_values[STYLE_SOURCE_RESOURCE_ID] = value_source_resid;
if (value.dataType != Res_value::TYPE_NULL || value.data == Res_value::DATA_NULL_EMPTY) {
indices_idx++;
//記錄數(shù)組的值法褥,ii即為屬性名的
out_indices[indices_idx] = ii;
}
//步長,out_values存放屬性值酬屉,類型等半等,因此需要步長來區(qū)分某個屬性存放塊的開始
out_values += STYLE_NUM_ENTRIES;
}
//out_indices 的第一個元素存放著找到有效屬性值的個數(shù)
out_indices[0] = indices_idx;
}
該方法比較長,省略了一些地方,主要做了兩件事:
1酱鸭、上面的1~4步驟實際上就是確定了加載屬性的優(yōu)先級
2、記錄查詢到的屬性值放在TypedArray里垛吗。
來看看和TypedArray關(guān)系
typedArray.getIndexCount():
public int getIndexCount() {
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
//這個值存放的是加載到有效屬性個數(shù)
return mIndices[0];
}
typedArray.getIndex(i);
public int getIndex(int at) {
//第一個元素記錄著個數(shù)凹髓,因此往后+1
return mIndices[1+at];
}
mIndices[]記錄著屬性名索引,還記得之前說過的在R.java里生成的
public static final int MyStyleable_attr_bool=0;
public static final int MyStyleable_attr_int=1;
public static final int MyStyleable_attr_ref=2;
public static final int MyStyleable_attr_str=3;
記錄著就是如上的值怯屉。而這些又可以索引到具體的屬性:
public static final int[] MyStyleable={
0x7f02002b, 0x7f02002c, 0x7f02002d, 0x7f02002e
};
再來看看獲取屬性值:
indexValue = typedArray.getIndex(i);
typedArray.getString(indexValue);
最終從TypedArray int[] mData里尋找蔚舀,該數(shù)組在上面的ApplyStyle里填充。
最后來直觀理解typedArray.getIndexCount()與TypedArray的mLength關(guān)系
<com.fish.myapplication.attr.MyAttrView
app:attr_str="str in myLayout"
app:attr_bool="true"
android:layout_width="100px"
android:layout_height="100px">
</com.fish.myapplication.attr.MyAttrView>
#attrs.xml
<declare-styleable name="MyStyleable">
<attr name="attr_str" format="string"></attr>
<attr name="attr_bool" format="boolean"></attr>
<attr name="attr_int" format="integer"></attr>
<attr name="attr_ref" format="reference"></attr>
</declare-styleable>
以上我們只是定義了兩個屬性锨络,而MyStyleable里聲明了4個屬性赌躺,因此TypedArray mIndices[] 有效屬性個數(shù)為2。而mLength 表示mIndices[]數(shù)組長度羡儿。
值得注意的是:
TypedArray 實例是可以復(fù)用的礼患,mIndices[] 長度只會變長。因此也許你調(diào)試的時候發(fā)現(xiàn)mIndices[] 并不一定等于4掠归,有可能更大缅叠。
以上就是自定義屬性相關(guān)的分析。