Android 自定義View:深刻理解自定義屬性(七)

引言

對於自定義屬性,遵循如下幾步,就能夠實現:php

  1. 自定義一個CustomView(extends View 或者 ViewGroup )類
  2. 編寫values/attrs.xml,在其中編寫styleableattr等標籤元素
  3. 在佈局文件中CustomView使用自定義的屬性
  4. 在CustomView的構造方法中經過TypedArray獲取

那麼,我有幾個問題,若是回答的很好,下面的文章就不用看了,能夠跳過:java

  • 以上步驟是如何奏效的?
  • styleable 的含義是什麼?能夠不寫嘛?我自定義屬性,我聲明屬性就行了,爲何必定要寫個styleable呢?
  • 若是系統中已經有了語義比較明確的屬性,我能夠直接使用嘛?
  • 構造方法中的有個參數叫作AttributeSet(eg: CustomView(Context context, AttributeSet attrs))這個參數看名字就知道包含的是參數的數組,那麼我能不能經過它去獲取個人自定義屬性呢?
  • TypedArray是什麼鬼?從哪冒出來的,就要我去使用?

自定義屬性使用示例

  1. 自定義屬性的聲明文件以下:
    <?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>
    複製代碼
  2. 自定義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();
        }
    
       ···
    }
    複製代碼
  3. 佈局文件使用
    <?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> 複製代碼
  4. 運行結果以下:

注意:個人styleable的name寫的是CustomViewTest,因此說這裏並不要求必定是自定義View的名字android

AttributeSet 與 TypedArray

構造方法中的有個參數叫作AttributeSet(eg: MyTextView(Context context, AttributeSet attrs) )這個參數看名字就知道包含的是參數的集合,那麼我能不能經過它去獲取個人自定義屬性呢?數組

首先AttributeSet中的確保存的是該View聲明的全部的屬性,而且外面的確能夠經過它去獲取(自定義的)屬性,怎麼作呢?以下:bash

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);
        }
    }
複製代碼

輸出:app

咦,真的能夠得到全部的屬性。經過AttributeSet能夠得到佈局文件中定義的全部屬性的key和value,那麼是否是說TypedArray就能夠拋棄了呢?答案是:NO,NO,No,重要的事,說三遍!。ide

TypedArray是什麼?

  1. 如今簡單修改一下佈局文件爲:
    <com.zeroxuan.customviewtest.CustomView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:testInteger="10086"
        app:testText="@string/my_name" />
    複製代碼
  2. 解析過程
    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();
    }
    複製代碼
  3. 運行結果

經過運行結果能夠看出,使用AttributeSet獲取的值,若是是引用都變成了@+數字的字符串。你說,這玩意你能看懂麼?那麼你看看最後一行使用TypedArray獲取的值,是否是瞬間明白了。函數

TypedArray其實就是用來簡化咱們解析自定義屬性工做的。好比上例,若是佈局中的屬性的值是引用類型,若是使用AttributeSet去得到最終的testText取值,那麼須要第一步拿到id,第二步再去解析id。而TypedArray正是幫咱們簡化了這個過程佈局

若是經過AttributeSet獲取最終的testText取值的過程以下:post

//使用索引 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的詳細說明

  1. obtainStyledAttributes(int[] attrs):從當前系統主題中獲取 attrs 中的屬性,最終調用是
    public TypedArray obtainStyledAttributes(@StyleableRes int[] attrs) {
            return mThemeImpl.obtainStyledAttributes(this, null, attrs, 0, 0);
        }
    複製代碼
  2. 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);
        }
    複製代碼
  3. obtainStyledAttributes(AttributeSet set, int[] attrs):從 layout 設置的屬性中獲取 attrs 中的屬性。
    public final TypedArray obtainStyledAttributes(
            AttributeSet set, @StyleableRes int[] attrs) {
        return getTheme().obtainStyledAttributes(set, attrs, 0, 0);
    }
    複製代碼
  4. 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屬性賦值時起做用.

這麼說可能有點迷糊,來一個例子但願你能立馬領悟!

  1. 首先自定義屬性
    <?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中的第三個參數。
  2. 定義StyleTheme
    <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>
    複製代碼
  3. 自定義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();
        }
    }
    複製代碼
  4. 佈局界面
    <com.zeroxuan.customviewtest.CustomView
        style="@style/style_CustomViewStyle"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:testText1="Direct declare in XML" />
    複製代碼
  5. 運行結果

調用順序

  1. 優先,取在佈局中給定的值
  2. 次之,取在佈局中設置的style中的值
  3. 其次,從defStyleAttr和defStyleRes中取值,注意若是 defStyleAttr有值,則再也不去defStyleResult中的值,就算defStyleAttr有的屬性沒有賦值。(具體看上面的打印結果)
  4. 最後使用,Theme中設置的屬性

注意 defStyleAttr的值必定要在Theme中設置纔有效果,就拿上面的例子說,若是你沒有在Theme中給R.attr.attr_defStyle賦值,而是直接在佈局文件中賦值,這樣作是沒有效果的。

@ 和?符號的引用區別

@? 符號的引用在使用時都有一個規範的格式:「@[+][package:]type:name」「?[package:][type:]name」。能夠看到,兩者均包含引用符號、資源所屬的包、資源類型和資源名稱。

@

@ 符號用於引用系統和咱們在項目中添加的一些固有資源(drawable,string 等),或者定義的 style 樣式。好比:

android:text="@string/app_name"
複製代碼

這裏的 app_name 就是咱們本身定義在項目文件 values/strings.xml 中的字符串資源。

android:text="@android:string/cancel"
複製代碼

而這裏的cancel屬於Android SDK中的系統字符串資源,因此須要添加 @android: 來指明引用來源。android:package:的一個具體實例。

?

?符號用於引用當前主題中定義的一些屬性值。

注意,? 符號經過屬性名字間接引用當前主題中的對應屬性值,而不是屬性自己。

舉個例子:

android:divider="?android:listDivider"
複製代碼

這裏的 ? 符號經過屬性名 android:listDivider 間接獲取當前主題賦予該屬性的值。如同 @android: 通常,?android: 表示該值源自 Android SDK 系統屬性。因爲在當前主題中尋找對應屬性名的值,因此沒有指定屬性類型,其實等同於:?android:attr/listDivider

那如何引用項目中自定義的屬性呢?

咱們在 attrs.xml 中定義一個屬性,如:

<declare-styleable name="CustomTextView">
    <attr name="colorTextCustom" format="reference|color"/>
</declare-styleable>
複製代碼

顯然,此時咱們定義的 colorTextCustom 屬性是沒有值的,直接引用沒有任何做用。須要在主題 style 中賦值:

<style name="BaseTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="colorTextCustom">#FF0000</item>
</style>
<style name="AppTheme" parent="BaseTheme">
    <item name="android:textColor">?colorTextCustom</item>
</style>
複製代碼

能夠看到,這裏在BaseTheme中對colorTextCustom屬性賦值,並在 AppTheme中經過「?colorTextCustom」 引用該屬性值。因爲是本地項目中定義的屬性,因此沒有添加 android: 命名空間。其實,這種作法的好處是,AppTheme所覆蓋的 View 都可經過構造函數獲取當前主題中的 colorTextCustom 屬性值。

?引用屬性寫法:

引用自定義屬性:?attr/colorPrimary 簡寫:?colorPrimary

引用系統屬性:?android:attr/colorPrimary 簡寫:?android:colorPrimary

其中,自定義屬性會複寫系統屬性

目錄結構

相關文章
相關標籤/搜索