引言
對於自定義屬性,遵循以下幾步,就可以實現:
- 自定義一個
CustomView
(extends View 或者 ViewGroup )類 - 編寫
values/attrs.xml
,在其中編寫styleable
和attr
等標籤元素 - 在佈局檔案中
CustomView
使用自定義的屬性 - 在CustomView的構造方法中通過TypedArray獲取
那麼,我有幾個問題,如果回答的很好,下面的文章就不用看了,可以跳過:
- 以上步驟是如何奏效的?
- styleable 的含義是什麼?可以不寫嘛?我自定義屬性,我宣告屬性就好了,為什麼一定要寫個styleable呢?
- 如果系統中已經有了語義比較明確的屬性,我可以直接使用嘛?
- 構造方法中的有個引數叫做
AttributeSet
(eg:CustomView(Context context, AttributeSet attrs)
)這個引數看名字就知道包含的是引數的陣列,那麼我能不能通過它去獲取我的自定義屬性呢? TypedArray
是什麼鬼?從哪冒出來的,就要我去使用?
自定義屬性使用示例
- 自定義屬性的宣告檔案如下:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CustomViewTest"> <attr name="testText" format="string"/> <attr name="testInteger" format="integer"/> </declare-styleable> </resources> 複製程式碼
- 自定義CustomView
public class CustomView extends View { ··· public CustomView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomViewTest); String testText = a.getString(R.styleable.CustomViewTest_testText); int testInteger = a.getInteger(R.styleable.CustomViewTest_testInteger, 10); Log.e(TAG, "testText =" + testText + " ,testInteger=" + testInteger); a.recycle(); } ··· } 複製程式碼
- 佈局檔案使用
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout ··· <!-- 自動查詢屬性 --> xmlns:app="http://schemas.android.com/apk/res-auto" "> <com.zeroxuan.customviewtest.CustomView android:layout_width="match_parent" android:layout_height="match_parent" app:testInteger="10086" app:testText="zeroXuan" /> </android.support.constraint.ConstraintLayout> 複製程式碼
- 執行結果如下:
注意:我的styleable的name寫的是CustomViewTest,所以說這裡並不要求一定是自定義View的名字
。
AttributeSet 與 TypedArray
構造方法中的有個引數叫做
AttributeSet
(eg: MyTextView(Context context, AttributeSet attrs) )這個引數看名字就知道包含的是引數的集合,那麼我能不能通過它去獲取我的自定義屬性呢?
首先AttributeSet
中的確儲存的是該View
宣告的所有的屬性,並且外面的確可以通過它去獲取(自定義的)屬性,怎麼做呢?如下:
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
final int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
Log.e(TAG, "attrName= " + attrName + " ,attrValue=" + attrValue);
}
}
複製程式碼
輸出:
咦,真的可以獲得所有的屬性。通過
AttributeSet
可以獲得佈局檔案中定義的所有屬性的key和value,那麼是不是說TypedArray
就可以拋棄了呢?答案是:NO,NO,No
,重要的事,說三遍!。
TypedArray是什麼?
- 現在簡單修改一下佈局檔案為:
<com.zeroxuan.customviewtest.CustomView android:layout_width="match_parent" android:layout_height="match_parent" app:testInteger="10086" app:testText="@string/my_name" /> 複製程式碼
- 解析過程
public CustomView(Context context, AttributeSet attrs) { super(context, attrs); final int count = attrs.getAttributeCount(); for (int i = 0; i < count; i++) { String attrName = attrs.getAttributeName(i); String attrValue = attrs.getAttributeValue(i); Log.e(TAG, "attrName= " + attrName + " ,attrValue=" + attrValue); } Log.e(TAG, ">> Use TypedArray"); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomViewTest); String testText = a.getString(R.styleable.CustomViewTest_testText); int testInteger = a.getInteger(R.styleable.CustomViewTest_testInteger, 10); Log.e(TAG, "testText =" + testText + " ,testInteger=" + testInteger); a.recycle(); } 複製程式碼
- 執行結果
通過執行結果可以看出,使用AttributeSet
獲取的值,如果是引用都變成了@+數字
的字串。你說,這玩意你能看懂麼?那麼你看看最後一行使用TypedArray獲取的值,是不是瞬間明白了。
TypedArray
其實就是用來簡化我們解析自定義屬性工作的。比如上例,如果佈局中的屬性的值是引用型別,如果使用AttributeSet去獲得最終的testText
取值,那麼需要第一步拿到id,第二步再去解析id。而TypedArray正是幫我們簡化了這個過程
。
如果通過AttributeSet獲取最終的testText
取值的過程如下:
//使用索引 3 ,是因為testText在CustomView中的索引是3
int resId=attrs.getAttributeResourceValue(3,-1);
Log.e(TAG, "attrName= "+getResources().getString(resId) );
複製程式碼
ok,現在別人問你TypedArray存在的意義,你就可以告訴他,
TypedArray
其實就是用來簡化解析自定義屬性工作流程的。
attr 和 declare-styleable的關係
首先要明確一點,attr
不依賴於declare-styleable
,declare-styleable
只是為了方便attr
的使用。
我們自己定義的屬性完全可以不放到declare-styleable
裡面,比如直接在resources檔案中定義一些屬性:
<attr name="custom_attr1" format="string" />
<attr name="custom_attr2" format="string" />
複製程式碼
定義一個attr
就會在R檔案裡面生成一個attr
型別的資源Id
,那麼我們去獲取這個屬性時,必須呼叫如下程式碼:
int[] custom_attrs = {R.attr.custom_attr1,R.custom_attr2};
TypedArray typedArray = context.obtainStyledAttributes(set,custom_attrs);
複製程式碼
而通過定義一個declare-styleable
,我們可以在R檔案裡自動生成一個int[]
,陣列裡面的int
就是定義在declare-styleable
裡面的attr的id
。所以我們在獲取屬性的時候就可以直接使用declare-styleable
陣列來獲取一系列的屬性。
<declare-styleable name="custom_attrs">
<attr name="custom_attr1" format="string" />
<attr name="custom_attr2" format="string" />
</declare-styleable>
複製程式碼
獲取:
TypedArray typedArray = context.obtainStyledAttributes(set,R.styleable.custom_attrs);
複製程式碼
如果系統中已經有了語義比較明確的屬性,我可以直接使用嘛?
答案是肯定的,可以使用,使用方式如下:
<declare-styleable name="test"> <!-- 使用系統屬性或者已經定義好的屬性,不需要去新增format屬性 --> <attr name="android:text" /> <attr name="testAttr" format="integer" /> </declare-styleable> 複製程式碼
然後在類中這麼獲取:
a.getString(R.styleable.CustomViewTest_android_text);
佈局檔案中直接android:text="zeroXuan is my name"
即可。
obtainStyledAttributes的詳細說明
- obtainStyledAttributes(int[] attrs):從當前系統主題中獲取 attrs 中的屬性,最終呼叫是
public TypedArray obtainStyledAttributes(@StyleableRes int[] attrs) { return mThemeImpl.obtainStyledAttributes(this, null, attrs, 0, 0); } 複製程式碼
- obtainStyledAttributes(int resid, int[] attrs):從資原始檔中獲取 attrs 中的屬性。
public TypedArray obtainStyledAttributes(@StyleRes int resId, @StyleableRes int[] attrs) throws NotFoundException { return mThemeImpl.obtainStyledAttributes(this, null, attrs, 0, resId); } 複製程式碼
- obtainStyledAttributes(AttributeSet set, int[] attrs):從 layout 設定的屬性中獲取 attrs 中的屬性。
public final TypedArray obtainStyledAttributes( AttributeSet set, @StyleableRes int[] attrs) { return getTheme().obtainStyledAttributes(set, attrs, 0, 0); } 複製程式碼
- obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes):下面細說。
public final TypedArray obtainStyledAttributes( AttributeSet set, @StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { return getTheme().obtainStyledAttributes( set, attrs, defStyleAttr, defStyleRes); } 複製程式碼
可以看出最終都是呼叫方法4,現在主要分析方法obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
,其中這個方法的四個引數解釋如下:
- AttributeSet set: 一個和xml中的標籤關聯的存放屬性的集合.
- int[] attrs: 我們要在xml中讀取的屬性.
- int defStyleAttr: 這是當前Theme中的包含的
一個指向style的引用
.當我們沒有給自定義View設定declare-styleable資源集合時,預設從這個集合裡面查詢佈局檔案中配置屬性值.傳入0表示不向該defStyleAttr中查詢預設值. - int defStyleRes: 這個也是
一個指向Style的資源ID
,但是僅在defStyleAttr為0或者defStyleAttr不為0但Theme中沒有為defStyleAttr屬性賦值時起作用.
這麼說可能有點迷糊,來一個例子希望你能立馬領悟!
- 首先自定義屬性
其中<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CustomView"> <attr name="testText1" format="string"/> <attr name="testText2" format="string"/> <attr name="testText3" format="string"/> <attr name="testText4" format="string"/> <attr name="testText5" format="string"/> <attr name="attr_defStyle" format="reference"/> </declare-styleable> </resources> 複製程式碼
attr_defStyle
屬性名,就是obtainStyledAttributes
中的第三個引數。 - 定義
Style
和Theme
<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="attr_defStyle">@style/style_attr_defStyleAttr</item> <item name="testText1">testText1-declare in Theme</item> <item name="testText2">testText2-declare in Theme</item> <item name="testText3">testText3-declare in Theme</item> <item name="testText4">testText4-declare in Theme</item> <item name="testText5">testText5-declare in Theme</item> </style> <!-- 用來表示 defStyleRes--> <style name="style_defStyleRes"> <item name="testText1">testText1-declare in style_defStyleRes</item> <item name="testText2">testText2-declare in style_defStyleRes</item> <item name="testText3">testText3-declare in style_defStyleRes</item> <item name="testText4">testText4-declare in style_defStyleRes</item> </style> <!-- 用來表示 attr_defStyleAttr 這個屬性的值 --> <style name="style_attr_defStyleAttr"> <item name="testText1">testText1-declare in style_attr_defStyleAttr</item> <item name="testText2">testText2-declare in style_attr_defStyleAttr</item> <item name="testText3">testText3-declare in style_attr_defStyleAttr</item> </style> <!-- 直接在佈局中的 style 中使用 --> <style name="style_CustomViewStyle"> <item name="testText1">testText1-declare in style_CustomViewStyle</item> <item name="testText2">testText2-declare in style_CustomViewStyle</item> </style> </resources> 複製程式碼
- 自定義View
public class CustomView extends View { private static final String TAG = "CustomView"; public CustomView(Context context) { this(context, null); } public CustomView(Context context, AttributeSet attrs) { //R.attr.attr_defStyle 就是defStyleRes this(context, attrs, R.attr.attr_defStyle); } public CustomView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, R.style.style_defStyleRes); } @TargetApi(21) public CustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); TypedArray a = context .obtainStyledAttributes(attrs, R.styleable.CustomView, defStyleAttr, defStyleRes); String text1 = a.getString(R.styleable.CustomView_testText1); String text2 = a.getString(R.styleable.CustomView_testText2); String text3 = a.getString(R.styleable.CustomView_testText3); String text4 = a.getString(R.styleable.CustomView_testText4); String text5 = a.getString(R.styleable.CustomView_testText5); Log.e(TAG, "text1== " + text1); Log.e(TAG, "text2== " + text2); Log.e(TAG, "text3== " + text3); Log.e(TAG, "text4== " + text4); Log.e(TAG, "text5== " + text5); a.recycle(); } } 複製程式碼
- 佈局介面
<com.zeroxuan.customviewtest.CustomView style="@style/style_CustomViewStyle" android:layout_width="match_parent" android:layout_height="match_parent" app:testText1="Direct declare in XML" /> 複製程式碼
- 執行結果
呼叫順序
優先
,取在佈局中給定的值次之
,取在佈局中設定的style中的值其次
,從defStyleAttr和defStyleRes中取值,注意如果 defStyleAttr有值,則不再去defStyleResult中的值,就算defStyleAttr有的屬性沒有賦值。(具體看上面的列印結果)最後使
用,Theme中設定的屬性
注意 defStyleAttr的值一定要在Theme中設定才有效果,就拿上面的例子說,如果你沒有在Theme中給R.attr.attr_defStyle賦值,而是直接在佈局檔案中賦值,這樣做是沒有效果的。