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

zeroXuan發表於2019-05-07

引言

對於自定義屬性,遵循以下幾步,就可以實現:

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

那麼,我有幾個問題,如果回答的很好,下面的文章就不用看了,可以跳過:

  • 以上步驟是如何奏效的?
  • 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. 執行結果如下:
    Android 自定義View:深入理解自定義屬性(七)

注意:我的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);
        }
    }
複製程式碼

輸出:

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

咦,真的可以獲得所有的屬性。通過AttributeSet可以獲得佈局檔案中定義的所有屬性的key和value,那麼是不是說TypedArray就可以拋棄了呢?答案是:NO,NO,No,重要的事,說三遍!。

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. 執行結果
    Android 自定義View:深入理解自定義屬性(七)

通過執行結果可以看出,使用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的詳細說明

  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. 執行結果
    Android 自定義View:深入理解自定義屬性(七)

呼叫順序

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

注意 defStyleAttr的值一定要在Theme中設定才有效果,就拿上面的例子說,如果你沒有在Theme中給R.attr.attr_defStyle賦值,而是直接在佈局檔案中賦值,這樣做是沒有效果的。

目錄結構

相關文章