屬性,樣式,主題以及實踐(attr, style, theme)

http://www.jianshu.com/p/3c4dc799e6db

0. 前言

最近寫Android app時要美化外觀了,但是發現自己對attr,style,theme這幾個概念理解的比較模糊,不知道哪些應該定義在styles.xml中,哪些應該定義在theme中,從而不知道好的實踐是什麼,因此也寫不出清晰,分離的代碼。

Google了一些資源,現總結如下。

1. 屬性,樣式,主題(attr, style, theme)

attr

每種View都有屬性,不管是自定義view,還是內置的view, 都要定義一些屬性,對於自定義的view,官方文檔中說明定義屬性的方法。
在項目的attrs.xml文件中,添加

<resources>
   <declare-styleable name="PieChart">
       <attr name="showText" format="boolean" />
       <attr name="labelPosition" format="enum">
           <enum name="left" value="0"/>
           <enum name="right" value="1"/>
       </attr>
   </declare-styleable>
</resources>

其中一個良好的規範是declare-styleable的name一般定義爲View的名字,但既然是規範而不是必須就說明也可以不這樣,至於爲什麼,下文會講到。

對於內置的view,其實和自定義view也一樣,只不過Google已經幫你定義好了。那麼其屬性的定義必定也是類似於以上這種形式。

那麼相關的代碼在哪裏呢?

就在SDK下的attrs.xml中,在這個文件中搜索view的名稱就能找到可用的屬性了(當然也可在官方文檔中找到其支持的屬性)

還有一個關鍵的地方,以上的講解給人一種屬性必定屬於某一view的印象,其實屬性是可以不屬於任何view的。我們可以這樣定義。

<resources>
    <attr name="customattr" format="boolean"/>
</resources>

和前面對比,是不是覺得在\<declare-styleable\>下定義就屬於某一view,在\<resources\>下定義就不屬於某一view?

其實不是這樣的,屬性是有一個全局空間的,無論是在\<declare-styleable\>下,還是在\<resources\>下定義的都屬於這個全局空間,比如說,對於以上兩種情況定義的屬性,我們可以這樣R.attrs.customattrR.attrs.showText來引用,可見與其是否在\<declare-styleable\>沒有關係。

既然屬性都在一個全局空間中,那麼就不允許同名不同類型的屬性定義,如以下是錯誤的。

<resources>
    <attr name="customattr" format="boolean"/>
    <attr name="customattr" format="string"/>
</resources>

好,到這裏,是不是有了很多疑問。如

  1. 什麼情況下屬性要定義在\<declare-styleable\>下,什麼情況下不需要
  2. \<declare-styleable\>及其name值是做什麼用的?爲什麼在其下定義屬性就和view關聯上了?

先說第二個問題。

要想理解第二個問題,關鍵的一個地方是理解obtainStyledAttributes函數。而該函數一般是在view的構造函數中調用的。繼續來看官方文檔中的例子。

public PieChart(Context context, AttributeSet attrs) {
   super(context, attrs);
   TypedArray a = context.getTheme().obtainStyledAttributes(
        attrs,
        R.styleable.PieChart,
        0, 0);

   try {
       mShowText = a.getBoolean(R.styleable.PieChart_showText, false);
       mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
   } finally {
       a.recycle();
   }
}

注意obtainStyledAttributes的參數R.styleable.PieChart,R說明是一個資源,styleable是資源類型,PieChart是資源的名稱,
styleable是由\<declare-styleable\>生成的,而PieChart就是對應\<declare-styleable\>的name屬性。所以說\<declare-styleable\>的name可以是任意值,只要在obtainStyledAttributes傳入相應的R.styleable.xxx就行了,但是無意義的值將造成代碼的難讀。

同時,從以上代碼也可知道\<declare-styleable\>起的作用主要是爲了方便將一組屬性組織到一起,方便在view的構造函數中獲得其屬性值。

至於第1個問題,在主題一節會講到。

style

前面已經說到如何定義屬性,但是屬性要有值啊,屬性的值在哪裏指定的呢?

最簡單的方法在Layout文件中定義,像這樣:

<TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:textColor="#00FF00"
    android:typeface="monospace"
    android:text="@string/hello" />

但是這樣不太好,我們知道CSS就是將外觀樣式從HTML中分離出來,同樣的思想用到這裏,可以這樣。

<TextView
    style="@style/CodeFont"
    android:text="@string/hello" />

<resources>
    <style name="CodeFont" parent="@android:style/TextAppearance.Medium">
        <item name="android:layout_width">fill_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textColor">#00FF00</item>
        <item name="android:typeface">monospace</item>
    </style>
</resources>

從上可以看出,樣式爲就是爲屬性進行賦值。

現在來談一談內置view的樣式。

我們平時使用一個Button,一個TextView,一個ImageView,很多屬性值並沒有指定,但依然可以顯示出某種樣式,既然有樣式就一定是爲相應屬性賦值了,那麼在哪裏呢?

定義的地方在SDK的styles.xml中,該文件中定義了很多style,其名稱表明了style的樣式,這也是一個好的命令規範。

其實,影響一個view的style並非只有這一個文件,我們知道,影響一個界面外觀的還有一個因素,那就是主題因素。至於主題和樣式的關係,以及兩者是如何影響到一個view的外觀的,在講解了主題之後,通過分析一個view的源碼來說明這個問題。

theme

Android系統已經內置了很多主題,這些主題的定義是在SDK的themes.xml文件中,打開該文件,內容如下

<resources>

    <style name="Theme">

        <item name="isLightTheme">false</item>
        <item name="colorForeground">@color/bright_foreground_dark</item>
        <item name="colorForegroundInverse">@color/bright_foreground_dark_inverse</item>
        <item name="colorBackground">@color/background_dark</item>
        <item name="colorBackgroundFloating">?attr/colorBackground</item>
        <item name="colorBackgroundCacheHint">?attr/colorBackground</item>

        ………………

我們發現,主題實際上也是一種style資源,其代碼結構與styles.xml中的一樣,那麼爲什麼要弄兩個文件,themes.xml與styles.xml有什麼不同呢?

雖然兩者都是style,但關注的面不一樣,styles.xml關注局部,而themes.xml關注整體風格。

什麼是局部,什麼是整體?看個例子就知道了。從styles.xml和themes.xml中各抽取一個。

styles.xml

<style name="Widget.Button.Transparent">
    <item name="background">@drawable/btn_default_transparent</item>
    <item name="textAppearance">?attr/textAppearanceSmall</item>
    <item name="textColor">@color/white</item>
</style>

從名字中可以看出,這是一個透明的Button,如果爲某個Button設爲該style,則該Button就是透明的。這裏隻影響到一個Button。

themes.xml

<style name="Theme">
    <item name="buttonStyle">@style/Widget.Button</item>

如果應用該主題,則會影響到該應用的所以Button樣式。

這裏就是整體與局部的區別。

我們知道,無論是style還是theme,都是爲了屬性賦值,以上面兩段代碼爲例,第一段會影響到button的background, textAppearance,textColor屬性,而對於themes.xml中的buttonStyle,查詢attrs.xml,可以看出button並沒有這個屬性(因爲Button繼承自TextView以及TextView繼承View,所以也要在這兩個View中找)。那麼這個button不支持的屬性是如何應用到該Button中呢?

我們注意到該屬性的類型是一個引用類型,系統會解析這個引用,最終會將Widget.Button樣式應用到該Button中,而該樣式中的屬性都是Button支持的,所以沒有問題。至於具體細節,在下一節中結合具體代碼講一下。

那麼,在平時開發應用程序時,如何利用好style.xml和themes.xml呢?

由上討論可知,我們可以利用style.xml定義一些局部的樣式,將外觀從layout文件中分離,定義style中,把注意力集中在某一個view上面。

而在寫themes.xml中,把注意力放在整個app的風格上面。爲一些帶有全局性含義的屬性賦值,如

  • colorPrimary
  • buttonStyle
  • textSize

等等。

2. 分析源碼,一個例子

以上分別介紹了attr, style, theme,下面以Button這個view爲例講一下系統是如果將theme中定義的樣式指定到某一局部Button的。

爲應用指定內置的主題

android:theme="@android:style/Theme"

看一下這個主題和Button相關的style,從themes.xml中得到如下信息

<item name="buttonStyle">@style/Widget.Button</item>

現在轉到Button的構造函數,假如以以下方式生成一個Button

Button button = new Button(this);

該構造函數源碼如下

Button(Context context) {
    this(context, null);
}

public Button(Context context, AttributeSet attrs) {
    this(context, attrs, com.android.internal.R.attr.buttonStyle);
}

public Button(Context context, AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
}

public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
}

可知最後調用的是父類的構造函數,而Button的父類是TextView,傳入的參數分別如下

context = thix
attrs = null
defStyleAttr = com.android.internal.R.attr.buttonStyle
defStyleRes = 0

現在轉到TextView的構造函數(以下省略了很多,只包含有關內容)

public TextView(
            Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);


        //得到當前應用的主題,theme中保存的就是主題的信息,這裏
        //就是內置的Theme主題信息
        final Resources.Theme theme = context.getTheme();

        a = theme.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);

        a = theme.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);

        a = context.obtainStyledAttributes(
            attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);

}

以上調用了obtainStyledAttributes函數三次,該函數的作用是獲得屬性的值。

其中第二個參數指明瞭要獲得哪些屬性的值。

第一個參數,第三個參數和第四個參數是值的三種來源,優先級爲

第一個參數 > 第二個參數 > 第三個參數

第一個參數爲一個鍵值對集合,如果要獲得值的屬性在第一個參數中,則使用第一個參數。

如果第一個參數爲空,就使用第三個參數,第三個參數就是當前主題中的一個屬性,該屬性是一個引用,引用style中的某一個具體的style.

如果第三個參數爲0,就使用第四個參數,該參數直接指定一個style resource,從該resource中取得所需要屬性的值。

在本例中,第一個參數爲空,所以使用的是第三個參數,而第三個參數就是從主題中獲得屬性的值,這裏是buttonStyle屬性,而該屬性又引用的styles.xml中的Widget.Button樣式,所以最終使用在Button上的就是該樣式了。

以上就是主題所定義的style如何應用到View的大概過程。

3. 好的實踐

  1. 利用style.xml定義一些局部的樣式,將外觀從layout文件中分離,定義style中,把注意力集中在某一個view上面。

    而在寫themes.xml中,把注意力放在整個app的風格上面。爲一些帶有全局性含義的屬性賦值,如

    • colorPrimary
    • buttonStyle
    • textSize

    等等。

  2. color.xml不代表樣式,只表示調色板

  3. dimens.xml和color一樣
  4. 爲了保持風格的一致性,優先使用主題中定義的風格,使用方式如下

    對於內置的主題

    ?android:attr/xxx

    對於自定義主題

    ?attr/xxx

    以上可以使應用的風格和諧統一

4. Q&A

  1. 如果想繼承某個主題並修改某些屬性,如何知道有哪些屬性可以修改

    A: 從源文件中找,如themes.xml

  2. 如何知道內置了哪些style

    A: 源文件styles.xml, 或官方文檔 R.style

  3. 如何知道一個View有哪些屬性可用

    A: 源文件attrs.xml中搜索view名稱,或參考該View的官方文檔

  4. 查看某主題的外觀效果

    A: 可以Theme Editor中查看



文/lingnanlu(簡書作者)
原文鏈接:http://www.jianshu.com/p/3c4dc799e6db
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章