Android CustomView (三) : 自訂義屬性

YANBIN HUNG
14 min readMar 15, 2020

--

這系列拖的有點久了...主要是因為自訂義屬性其實蠻簡單的,只要依照 SOP 一步一步來就能完成了。而且關於這部分的網路文章也非常多,這樣寫起來也沒甚麼意義,幸運的是最近看到 Google 寫了一篇有關於 Them 跟 Style 的文章,非常有系統地去介紹這兩個的差別以及使用情境,於是本篇將會探討這部分與 CustomView 的連結。

Android styling — Theme vs Styles https://medium.com/androiddevelopers/android-styling-themes-vs-styles-ebe05f917578

Style V.S. Theme

這兩個真的很難分辨,尤其是在很多不管是官網或是其他的網路教學文章裡,看完了之後還是霧煞煞,只大概知道前者是用在單個 View 上面,後者是用在整個頁面,甚至是整個應用程式上。然後呢?知道了這些之後就能夠好好得分辨並使用它們了嗎?至少對我來說並不是。因為就算看過了這些知識,到頭來也不會去用 Theme,一般的 App 根本就不需要有不同的主題去作切換,因此,所有的設定都跑去 Style 了。然而我們不知道的是,其實有些設定應該是Theme 的責任。

A theme is a type of style that’s applied to an entire app, activity, or view hierarchy — not just an individual view. When you apply your style as a theme, every view in the app or activity applies each style attribute that it supports. Themes can also apply styles to non-view elements, such as the status bar and window background.(官網對於 Theme 的解釋,style 跟 theme 出現了很多次,看完這一段應該會更混亂了吧…)

這時候就會問了,這樣做有什麼後果嗎?用起來沒什麼問題啊?在 Android 10 之前的確是還好,但是現在我們可以開啟 Dark theme 的設定了,可以預見到,一部分的使用者將會在 play store 用評價“逼迫”你支援 Dark theme…

所以話又說回來了,什麼樣的設定應該屬於 Theme? 以前面的例子來說,Dark V.S. non Dark ,第一個想到的應該是顏色對吧,所以來看一下開啟新應用程式的 Theme 一定會有的三個顏色設定:colorPrimary, colorPrimaryDark 與 colorAccent 各代表什麼吧!

根據 Material Design GuidelinePrimary 是一個 App 最頻繁用到的顏色,像是 Toolbar 的背景,Button 的顏色等等。 除此之外,通常一個 App 會想要有稍微深一點的或是淺一點的主色變化版,而這顏色就是 PrimaryDark。 這時候就會很想問:那為什麼不是 colorPrimaryLight 呢?恩…我也不知道答案。我猜 Google 有意識到這件事所以後來才建議大家去用 Material Theme,恩…有點扯太遠了,再拉回來看 colorAccent 吧。關於 ColorAccent 的資料真的是少的可憐,其中第一條線索是官網中的這段說明:

這樣的說明實在讓我很疑惑,為什麼是 secondary ?accent 跟 secondary 的關係是什麼?那要是拿 accent color 去搜尋會得到什麼結果呢?

根據以上的說明我們可以推敲出 accent color 是用來做強調、凸顯某項事物所存在的顏色,因此顏色必須不能跟 colorPrimary 太過相近(最好是對比色),像是 CheckBox 或是 Toggle 的顏色就需要夠明顯,使用者就會比較容易將注意力集中到這些選項上面。

來看看下面這個例子:ColorPrimary 就是上面那條 ToolBar 的背景顏色,ColorPrimaryDark 是 Status bar 的顏色,而 CheckBox 跟 EditText 的 focus 的顏色則是 ColorAccent。

那如果我想要在CheckBox 的 textColor 使用 colorPrimary 怎麼辦呢?有下列兩種選項:

這兩個的差別是什麼呢?第一種會使用在 CheckBox 所在的 Context 底下 Theme 的設定,第二種是直接使用指定的顏色,那麼哪一種比較好呢?目前看起來都會顯示同一個顏色,可能沒什麼差別…? 這邊先留給讀者思考一下。

總結一下,Theme 除了用來定義整個 Application 的風格之外,每一個屬性(colorPrimary, colorAccent)背後都是有它想表達的意義,而且 Theme 這個系統希望我們能好好的運用這個機制(但通常沒有…),來減少重複的屬性設定。除了這三個顏色之外,還有許多基礎的設定值,像是文字顏色,大小等等。

另一方面,Style 是設定單一元件用的,Parent View 的 Style 並不會影響到 child View 的 Style。而且跟 Theme 一樣,是藉由 xml 來做設定的,實際上可能會是長這樣:

啥??不是跟 Theme 差不多嗎?一樣都是 style 開頭,一樣都是 key-value 的屬性設定,這樣不就越搞越混了嗎?這也是我覺得他們設計不好的地方,如果 Theme 的 tag 是 <Theme name="..." ,不就好很多了嗎?

嫌歸嫌,我們沒辦法改變現狀。這裡還是有些差別的,首先第一個不一樣的地方是 parent ,如果是 Theme 相關的設定會是 Theme.xxx 開頭,Style 則會是含有 Widget 這個關鍵字。另一個不一樣的地方是,Style 可以套用 Theme 的設定,反之不行,這句話是什麼意思呢?請看以下範例:

Theme 跟 Style 不一樣之處

android:textColor 不再是寫死的值了,換成了當前 Theme 的 colorAccent 。所以說,MyButton的設定會因為不同的 Theme 而有所改變,而 Theme 的設定不管是放在同一個 View ,ParentView 或是 Activity ,都可以影響到這邊的 android:textColor 。(舉例)

假設現在多一個 BlueTheme,內容如下:

//AndroidManifest.xml
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppBlueTheme">

執行之後可以看到不只是 Toolbar 的顏色改了,連 Button 的文字也一起改了!

另外請注意到上方的 CheckBox 的文字顏色,為什麼他是上一個 Theme 的 colorAccent 呢?因為在 xml 中我直接指定他的顏色是 @Color/colorAccent 而不是 ?attr/colorAccent ,所以不管 Theme 怎麼換都不會影響到他。

那我們要如何使用 Theme 去設定 CheckBox 的顏色呢?Android 系統中有一個預設的 Style: android:checkboxStyle ,可以使用下面的做法去覆寫:

請注意:如果 layout xml 的 android:textColor 有做設定的話,是會以 layout 的設定為優先的,這時候 Theme 是沒有效果的!讀者可以另外實驗看看如果使用 Style 會有什麼影響。

總結以上觀察,得到下面幾點結論:

  • 如果該屬性希望因為不同 Theme 而改變,請盡量使用 ?attr/
  • 藉由覆寫 Android 的 Style 設定可以避免在不同位置的 View (例如 CheckBox)上寫重複的設定
  • 設定的優先權:View attribute > Style > Theme

關於優先權可以參考這篇文章: https://medium.com/androiddevelopers/whats-your-text-s-appearance-f3a1729192d

所以 CustomView 需要注意什麼?

理所當然的,我們所做的 CustomView 也希望能夠跟 Theme 同步,如此一來,切換不同 Theme 的時候就會有足夠的一致性。以下三種層級都可以達到這個目的:

  • Attribute — 在 layout xml 設定屬性時使用 ?attr
  • Style — 定義新的 Style ,就跟上面的 android:checkboxStyle 一樣。如此一來就可以作更加自由的設定了。
  • Them — CustomView 可以直接拿到Theme 的設定,像是colorAccent , colorPrimary

先從最簡單的第一層開始吧!還記得我們的長條圖嗎?上一次實作了橫軸跟縱軸的顯示文字,現在來試試看更改文字的顏色吧!首先,在我們的 xml 加上 android:textColor=#008800(為了方便測試,這邊沒有使用 ?attr):

<com.yanbin.chart.BarChart
android:id="@+id/barChart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:padding="8dp"
android:textColor="#008800"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

在進行下一步之前,需要瞭解一下 Android View 實作的機制:當 Android 的系統呼叫 layoutInflater.inflate() 的時候,這些在 xml 的設定將會儲存到 AttributeSet 這個類別裡,然後會將這個 AttributeSet 以及 Context當作參數,建立 View 的實例,也就是呼叫 View 的第二個建構子:

class BarChart: View {    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) ...

依據上面的推導,得知 android:textColor的值可以由 attrs 來獲取,那要怎麼拿呢?很不幸的,官方不建議我們直接從 AttributeSet 獲取,而是要再隔一層:

看看上面的第 28 行,獲得 attributes 的責任委派給了 themethemeobtainStyledAttributes會回傳一個 TypedArray 。再來可以藉由回傳回來的 TypedArray去獲取這些設定。當然,這是因為有 AttributeSet 當作obtainStyledAttributes這個函式的參數才能正確得拿到設定。

接下來看看第二個參數:attrArray 。這個陣列的作用是,我可以指定要哪些屬性,在這裡我們要的是android.R.attr.textColor。另外一點重要的是,這個陣列裡的資料位置,將會與回傳的 typedArray 裡每一個設定的位置一樣 。由於attrArray的元素只有一個 android.R.attr.textColor,而且 index 是從 0 開始算的,所以呼叫 getColor 的 index 要寫 0。依此類推,如果有第二、三個設定, index 就會是 1、2。

除了 getColor 之外,typedArray 還提供了其他 api 像是:

  • getString
  • getDrawable
  • getDimension

最後要記得,typedArray 用完了要去呼叫 recycle()。以下是執行的結果:

自定義 Style

有看過官方教學的讀者應該會知道, CustomView 通常需要建立 declare-styleable 的 Resource ,為什麼上面沒這樣做呢?繼續看下去就會知道了:依照官方的範例的話,我們需要新增一個 declare-styleable 的 resource ,而且 name要跟 CustomView 的 Class name 一樣,這裏的 Class name 是 BarChart。再來,新增一個名為 textColor 的屬性,如下:

BarChart 的建構子則是像下面這樣實作的:

這裏不一樣的地方出現了,原來的 attrArray 被替換為 R.stylable.BarChart,型別一樣是 IntArray。然後,拿值的 index 也換成了 R.styleable.BarChart_textColor 。那這些是怎麼冒出來的呢?答案是,當我們寫了上面的 declare-styleable 之後,系統會自動幫我們產生這些資源。最後,在 layout xml 中,屬性要使用的 key 要換成 app:textColor 而不是 android:textColor

這樣一來我們就得到了一樣的結果了。看到這裡,不知道讀者是不是會跟我一樣好奇,如果將 R.styleable.BarChart_textColor 改成 0 會不會也得到一樣的結果?如果結果一樣,為什麼?

挑戰一下

上面的內容很多很多,在繼續看下去之前,如果不自己試試看是會忘光的,假如今天我很頑皮的要求 xml 會是像下面這樣:

注意有兩個 textColor

我有辦法畫出這樣的 BarChart 嗎?

橫軸跟縱軸的文字顏色不一樣

答案將會在下一篇中揭曉。

結語(碎碎念)

寫這個主題花的時間比我想像中的多了很多,一方面是我自己也沒有對 Android Style & Theme 有全盤的了解,算是在邊寫文章邊學。另一方面是覺得我的表達方式沒有很好,一直寫了又刪,刪了又寫,而這篇你可能只看了兩分鐘的文章是我兩個下午的成果。

我其實也還沒嘗試過,藉由了解 Android 的機制來去做一個相容於 Theme system 的設計 ,下一篇將會推出一個我自己也會滿意的統整與分析(可能會是兩個禮拜後了…),謝謝大家收看。

--

--