Andro自定義View基礎之自定義屬性深入分析

前言:人總會疲憊,當感覺累的時候,再堅持一下,再堅持一下就好。

1.初始Custom View的構造函數
通常我們在實現Custom View的時候,都會先繼承View并實現View的三個構造函數,例如:

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;

public class MyCustomView extends View {
    /**
     * 第一個構造函數
     */
    public MyCustomView(Context context) {
        this(context, null);
    }

    /**
     * 第二個構造函數
     */
    public MyCustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * 第三個構造函數
     */
    public MyCustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // TODO:獲取自定義屬性
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }
}

1:在代碼中直接new一個Custom View實例的時候,會調用第一個構造函數.
2:在xml布局文件中調用Custom View的時候,確切的說是xml解析到當前的標簽的時候,會調用第二個構造函數.
3:在xml布局文件中調用Custom View,并且Custom View標簽中還有自定義屬性時,這里調用的還是第二個構造函數.
也就是說,系統默認只會調用Custom View的前兩個構造函數,至于第三個構造函數的調用,通常是我們自己在構造函數中主動調用的(例如,在第二個構造函數中調用第三個構造函數)

接下來就針對自定義View的屬性展開:

2.為什么要自定義屬性

要使用屬性,首先這個屬性應該存在,所以如果我們要使用自己的屬性,必須要先把他定義出來才能使用。但我們平時在寫布局文件的時候好像沒有自己定義屬性,但我們照樣可以用很多屬性,這是為什么?我想大家應該都知道:系統定義好的屬性我們就可以拿來用唄,但是你們知道系統定義了哪些屬性嗎?哪些屬性是我們自定義控件可以直接使用的,哪些不能使用?什么樣的屬性我們能使用?這些問題我想大家不一定都弄得清除,下面我們去一一解開這些謎團。
??系統定義的所有屬性我們可以在\sdk\platforms\android-xx\data\res\values目錄下找到attrs.xml這個文件,這就是系統自帶的所有屬性,打開看看一些比較熟悉的:

<declare-styleable name="View">
    <attr name="id" format="reference" />
    <attr name="background" format="reference|color" />
    <attr name="padding" format="dimension" />
     ...
    <attr name="focusable" format="boolean" />
     ...
</declare-styleable>

<declare-styleable name="TextView">
    <attr name="text" format="string" localization="suggested" />
    <attr name="hint" format="string" />
    <attr name="textColor" />
    <attr name="textColorHighlight" />
    <attr name="textColorHint" />
     ...
</declare-styleable>

<declare-styleable name="ViewGroup_Layout">
    <attr name="layout_width" format="dimension">
        <enum name="fill_parent" value="-1" />
        <enum name="match_parent" value="-1" />
        <enum name="wrap_content" value="-2" />
    </attr>
    <attr name="layout_height" format="dimension">
        <enum name="fill_parent" value="-1" />
        <enum name="match_parent" value="-1" />
        <enum name="wrap_content" value="-2" />
    </attr>
</declare-styleable>

<declare-styleable name="LinearLayout_Layout">
    <attr name="layout_width" />
    <attr name="layout_height" />
    <attr name="layout_weight" format="float" />
    <attr name="layout_gravity" />
</declare-styleable>

<declare-styleable name="RelativeLayout_Layout">
    <attr name="layout_centerInParent" format="boolean" />
    <attr name="layout_centerHorizontal" format="boolean" />
    <attr name="layout_centerVertical" format="boolean" />
     ...
</declare-styleable>

看看上面attrs.xml文件中的屬性,發現他們都是有規律的分組的形式組織的。以declare-styleable 為一個組合,后面有一個name屬性,屬性的值為View 、TextView 等等,有沒有想到什么?沒錯,屬性值為View的那一組就是為View定義的屬性,屬性值為TextView的就是為TextView定義的屬性…。

因為所有的控件都是View的子類,所以為View定義的屬性所有的控件都能使用,這就是為什么我們的自定義控件沒有定義屬性就能使用一些系統屬性。

但是并不是每個控件都能使用所有屬性,比如TextView是View的子類,所以為View定義的所有屬性它都能使用,但是子類肯定有自己特有的屬性,得單獨為它擴展一些屬性,而單獨擴展的這些屬性只有它自己能有,View是不能使用的,比如View中不能使用android:text=“”。又比如,LinearLayout中能使用layout_weight屬性,而RelativeLayout卻不能使用,因為layout_weight是為LinearLayout的LayoutParams定義的。

綜上所述,自定義控件如果不自定義屬性,就只能使用VIew的屬性,但為了給我們的控件擴展一些屬性,我們就必須自己去定義。

2. 怎樣自定義屬性

自定義屬性的步驟想必大家都所了解:
1、自定義一個CustomView(extends View )類
2、編寫values/attrs.xml,在其中編寫styleable和item等標簽元素
3、在布局文件中CustomView使用自定義的屬性(注意命名空間)
4、在CustomView的構造方法中通過TypedArray獲取

有幾個問題不知道是否知道:

  • 以上步驟是如何奏效的?

  • styleable 的含義是什么?可以不寫嘛?我自定義屬性,我聲明屬性就好了,為什么一定要寫個styleable呢?

  • 如果系統中已經有了語義比較明確的屬性,我可以直接使用嘛?

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

  • TypedArray是什么鬼?從哪冒出來的,就要我去使用?
    看完本篇文章,相信你會徹底解決上述問題。
    翻閱系統的屬性文件,你會發現,有兩種獲取形式(下面講);這兩種
    區別就是attr標簽后面帶不帶format屬性,如果帶format的就是在定義屬性,如果不帶format的就是在使用已有的屬性,name的值就是屬性的名字,format是限定當前定義的屬性能接受什么值。

    打個比方,比如系統已經定義了android:text屬性,我們的自定義控件也需要一個文本的屬性,可以有兩種方式:

第一種:我們并不知道系統定義了此名稱的屬性,我們自己定義一個名為text或者mText的屬性(屬性名稱可以隨便起的)

     <resources>
        <declare-styleable name="MyTextView">
           <attr name=“text" format="string" />
        </declare-styleable>
</resources>

第二種:我們知道系統已經定義過名稱為text的屬性,我們不用自己定義,只需要在自定義屬性中申明,我要使用這個text屬性
(注意加上android命名空間,這樣才知道使用的是系統的text屬性)

<resources>
    <declare-styleable name="MyTextView">
        <attr name=“android:text"/>
    </declare-styleable>
</resources>

為什么系統定義了此屬性,我們在使用的時候還要聲明?因為,系統定義的text屬性是給TextView使用的,如果我們不申明,就不能使用text屬性。

4. 屬性值的類型format

format支持的類型一共有11種:

(1). reference:參考某一資源ID

  • 屬性定義:
<declare-styleable name = "名稱">
     <attr name = "background" format = "reference" />
</declare-styleable>

  • 屬性使用:
<ImageView android:background = "@drawable/圖片ID"/>

(2). color:顏色值

  • 屬性定義:
<attr name = "textColor" format = "color" />

  • 屬性使用:
<TextView android:textColor = "#00FF00" />

(3). boolean:布爾值

  • 屬性定義:
<attr name = "focusable" format = "boolean" />

  • 屬性使用:
<Button android:focusable = "true"/>

(1). reference:參考某一資源ID

  • 屬性定義:
<declare-styleable name = "名稱">
     <attr name = "background" format = "reference" />
</declare-styleable>

  • 屬性使用:
<ImageView android:background = "@drawable/圖片ID"/>

(4). dimension:尺寸值

  • 屬性定義:
<attr name = "layout_width" format = "dimension" />

  • 屬性使用:
<Button android:layout_width = "42dip"/>

(5). float:浮點值

  • 屬性定義:
<attr name = "fromAlpha" format = "float" />

  • 屬性使用:
<alpha android:fromAlpha = "1.0"/>

(6). integer:整型值

  • 屬性定義:
<attr name = "framesCount" format="integer" />

  • 屬性使用:
<animated-rotate android:framesCount = "12"/>

(7). string:字符串

  • 屬性定義:
<attr name = "text" format = "string" />

  • 屬性使用:
<TextView android:text = "我是文本"/>

(8). fraction:百分數

  • 屬性定義:
<attr name = "pivotX" format = "fraction" />

  • 屬性使用:
<rotate android:pivotX = "200%"/>

(9). enum:枚舉值

  • 屬性定義:
<declare-styleable name="名稱">
    <attr name="orientation">
        <enum name="horizontal" value="0" />
        <enum name="vertical" value="1" />
    </attr>
</declare-styleable>

  • 屬性使用:
<LinearLayout  
    android:orientation = "vertical">
</LinearLayout>

注意:枚舉類型的屬性在使用的過程中只能同時使用其中一個,不能 android:orientation = “horizontal|vertical"

(10). flag:位或運算

  • 屬性定義:
<declare-styleable name="名稱">
    <attr name="gravity">
            <flag name="top" value="0x30" />
            <flag name="bottom" value="0x50" />
            <flag name="left" value="0x03" />
            <flag name="right" value="0x05" />
            <flag name="center_vertical" value="0x10" />
            ...
    </attr>
</declare-styleable>

  • 屬性使用:
<TextView android:gravity="bottom|left"/>

注意:位運算類型的屬性在使用的過程中可以使用多個值

(11). 混合類型:屬性定義時可以指定多種類型值

  • 屬性定義:
<declare-styleable name = "名稱">
     <attr name = "background" format = "reference|color" />
</declare-styleable>

  • 屬性使用:
<ImageView
android:background = "@drawable/圖片ID" />
或者:
<ImageView
android:background = "#00FF00" />

通過上面的學習我們已經知道怎么定義各種類型的屬性,以及怎么使用它們,但是我們寫好布局文件之后,要在控件中使用這些屬性還需要將它解析出來。

5. 類中獲取屬性值

在這之前,順帶講一下命名空間,我們在布局文件中使用屬性的時候(android:layout_width="match_parent")發現前面都帶有一個android:,這個android就是上面引入的命名空間xmlns:android="http://schemas.android.com/apk/res/android”,表示到android系統中查找該屬性來源。只有引入了命名空間,XML文件才知道下面使用的屬性應該去哪里找(哪里定義的,不能憑空出現,要有根據)。

如果我們自定義屬性,這個屬性應該去我們的應用程序包中找,所以要引入我們應用包的命名空間xmlns:itydl="http://schemas.android.com/apk/res-auto”,res-auto表示自動查找,還有一種寫法xmlns:itydl="http://schemas.android.com/apk/com.example.openxu.myview",com.example.itydl.myview為我們的應用程序包名。

按照上面學習的知識,我們先定義一些屬性,并寫好布局文件。
先在res\values目錄下創建attrs.xml,定義自己的屬性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyTextView">
        <!--聲明MyTextView需要使用系統定義過的text屬性,注意前面需要加上android命名-->
        <attr name="android:text" />
        <attr name="mTextColor" format="color" />
        <attr name="mTextSize" format="dimension" />
    </declare-styleable>
</resources>

注意,上面styleable的name寫的是MyTextView,這個其實不一定要寫自定義View的類名,其他的字母也是可以的。只不過規范化書寫一般會寫上針對哪個類的類名稱

在布局文件中,使用屬性(注意引入我們應用程序的命名空間,這樣在能找到我們包中的attrs):

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:itydl="http://schemas.android.com/apk/res-auto"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
        <com.example.itydl.myview.MyTextView
            android:layout_width="200dip"
            android:layout_height="100dip"
            itydl:mTextSize="25sp"
            android:text="我是文字"
            itydl:mTextColor ="#0000ff"
            android:background="#ff0000"/>
</LinearLayout>

在構造方法中獲取屬性值:

public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
    String text = ta.getString(R.styleable.MyTextView_android_text);
    int mTextColor = ta.getColor(R.styleable.MyTextView_mTextColor, Color.BLACK);
    int mTextSize = ta.getDimensionPixelSize(R.styleable.MyTextView_mTextSize, 100);
    ta.recycle();  //注意回收
    Log.v("itydl", “text屬性值:"+mText);
    Log.v("itydl", "mTextColor屬性值:"+mTextColor);
    Log.v("itydl", "mTextSize屬性值:"+mTextSize);
}

log輸出:

Paste_Image.png

6. Attributeset和TypedArray以及declare-styleable

Attributeset看名字就知道是一個屬性的集合,實際上,它內部就是一個XML解析器,幫我們將布局文件中該控件的所有屬性解析出來,并以key-value的兼職對形式維護起來。其實我們完全可以只用他通過下面的代碼來獲取我們的屬性就行。

public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    int count = attrs.getAttributeCount();
    for (int i = 0; i < count; i++) {
        String attrName = attrs.getAttributeName(i);
        String attrVal = attrs.getAttributeValue(i);
        Log.e("openxu", "attrName = " + attrName + " , attrVal = " + attrVal);
    }
}

log輸出:

Paste_Image.png

發現通過Attributeset獲取屬性的值時,它將我們布局文件中的值原原本本的獲取出來的,比如寬度200.0dip,其實這并不是我們想要的,如果我們接下來要使用寬度值,我們還需要將dip去掉,然后轉換成整形,這多麻煩。其實這都不算什么,更惡心的是,backgroud我應用了一個color資源ID,它直接給我拿到了這個ID值,前面還加了個@,接下來我要自己獲取資源,并通過這個ID值獲取到真正的顏色。

我們再換TypedArray試試。
使用TypedArray獲取屬性值:

public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
    String mText = ta.getString(R.styleable.MyTextView_android_text);
    int mTextColor = ta.getColor(R.styleable.MyTextView_mTextColor, Color.BLACK);
    int mTextSize = ta.getDimensionPixelSize(R.styleable.MyTextView_mTextSize, 100);
    float width = ta.getDimension(R.styleable.MyTextView_android_layout_width, 0.0f);
    float hight = ta.getDimension(R.styleable.MyTextView_android_layout_height,0.0f);
    int backgroud = ta.getColor(R.styleable.MyTextView_android_background, Color.BLACK);
    ta.recycle();  //注意回收
    Log.v("itydl", "width:"+width);
    Log.v("itydl", "hight:"+hight);
    Log.v("itydl", "backgroud:"+backgroud);
    Log.v("itydl", "mText:"+mText);
    Log.v("itydl", "mTextColor:"+mTextColor);
    Log.v("itydl", "mTextSize:"+mTextSize);ext, 0, mText.length(), mBound);
}

log輸出:

Paste_Image.png

看看多么舒服的結果,我們得到了想要的寬高(float型),背景顏色(color的十進制)等,TypedArray提供了一系列獲取不同類型屬性的方法,這樣就可以直接得到我們想要的數據類型,而不用像Attributeset獲取屬性后還要一個個處理才能得到具體的數據,實際上TypedArray是為我們獲取屬性值提供了方便,注意一點,TypedArray使用完畢后記得調用 ta.recycle();回收 。

7、快速獲取各屬性的方式:

你可能還見過如下方式自定義屬性的文章,其實接下來要說的就是一種快速獲取自定義屬性的方式:

<?xml version="1.0" encoding="utf-8"?>  
<resources>  

    <attr name="titleText" format="string" />  
    <attr name="titleTextColor" format="color" />  
    <attr name="titleTextSize" format="dimension" />  

    <declare-styleable name="CustomTitleView">  
        <attr name="titleText" />  
        <attr name="titleTextColor" />  
        <attr name="titleTextSize" />  
    </declare-styleable>  

</resources>  

然后在布局中聲明我們的自定義View

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"  
    xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent" >  

    <com.example.customview01.view.CustomTitleView  
        android:layout_width="200dp"  
        android:layout_height="100dp"  
        custom:titleText="3712"  
        custom:titleTextColor="#ff0000"  
        custom:titleTextSize="40sp" />  

</RelativeLayout>

在View的構造方法中,獲得我們的自定義的樣式

       /** 
         * 獲得我們所定義的自定義樣式屬性 
         */  
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);  
        int n = a.getIndexCount();  
        for (int i = 0; i < n; i++)  
        {  
            int attr = a.getIndex(i);  
            switch (attr)  
            {  
            case R.styleable.CustomTitleView_titleText:  
                mTitleText = a.getString(attr);  
                break;  
            case R.styleable.CustomTitleView_titleTextColor:  
                // 默認顏色設置為黑色  
                mTitleTextColor = a.getColor(attr, Color.BLACK);  
                break;  
            case R.styleable.CustomTitleView_titleTextSize:  
                // 默認設置為16sp,TypeValue也可以把sp轉化為px  
                mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(  
                        TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));  
                break;  

            }  

        }  
        a.recycle();

這種方式在實際開發中也是最常見的方式。

8.declare-styleable

他是用來干嘛的,如果不要它可不可以?
直接給答案吧。答案是可以的,我們自定義屬性完全可以寫成下面的形式:

<?xml version="1.0" encoding="utf-8"?>
<resources>
        <attr name="mTextColor" format="color" />
        <attr name="mTextSize" format="dimension" />
</resources>

之前的形式是這樣的:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyTextView">
        <attr name="android:text" />
        <attr name="android:layout_width" />
        <attr name="android:layout_height" />
        <attr name="android:background" />
        <attr name="mTextColor" format="color" />
        <attr name="mTextSize" format="dimension" />
    </declare-styleable>
</resources>

或者:
<?xml version="1.0" encoding="utf-8"?>
<resources>
          <!--定義屬性-->
       <attr name="mTextColor" format="color" />
        <attr name="mTextSize" format="dimension" />
    <declare-styleable name="MyTextView">
        <!--生成索引-->
        <attr name="android:text" />
        <attr name="android:layout_width" />
        <attr name="android:layout_height" />
        <attr name="android:background" />
        <attr name=“mTextColor" />
        <attr name="mTextSize"  />
    </declare-styleable>
</resources>

我們都知道所有的資源文件在R中都會對應一個整型常亮,我們可以通過這個ID值找到資源文件。
??屬性在R中對應的類是public static final class attr,如果我們寫了declare-styleable,在R文件中就會生成styleable類,這個類其實就是將每個控件的屬性分組,然后記錄屬性的索引值,而TypedArray正好需要通過此索引值獲取屬性。

public static final class styleable

          public static final int[] MyTextView = {
            0x0101014f, 0x7f010038, 0x7f010039
        };
        public static final int MyTextView_android_text = 0;
        public static final int MyTextView_mTextColor = 1;
        public static final int MyTextView_mTextSize = 2;
}

記住了嗎?如果不要它也是可以噠。
相信看完文章,自定義屬性相關的知識已經難不住你了~當然這也是自定義View必須掌握的基礎知識。

未完待續

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容