什麼是 Style
是一個 view 視圖外觀的屬性集合,透過它可以定義每個元件屬性的變化。例如文字的大小、顏色、字型、間距
給定每個 view 特定的屬性值來改變元件外觀,例如 button 的文字大小與顏色,都可以在每個元件當中的 style 屬性中去指定,將所有屬性提取到樣式中,可以在多個小部件中輕鬆使用和維護它們
應用上我們會將所需的屬性都義在 Style 中,需要時套用在元件身上
例如,在不使用 style 的情況下,想要一個有白字藍底且有圓角的 button,將會將所有的屬性都放在元件中
這時候就可以把這些屬性都把包在 style 當中,在需要這些元件設置為相同時套用即可,不需要再重複編寫這些屬性
什麼是 Theme
是一個可以套用在整個 app 應用視圖外觀上,不單單只是一個 view。當套用 theme 的時候,整個 app 當中的視圖元件都會支援與套用,還能應用於非視圖元素,例如 statusBar 和 windowBackground。透過 theme 的切換,可以讓 App 擁有不同的配色設計,例如讓用戶切換夜間模式與日間模式
一個 theme 定義了一組可以被重複引用的多個 style 資源檔案,裡面包含了各種元件的 style。在創建初期,color 的部分系統都幫我們設置好了,這些顏色會依據配置的規則,渲染在不同的元件上
簡單說,在 Theme 當中,我們可以透過不同元件 xx_Style 屬性,讓 App 內所有的元件都擁有相同的屬性外觀,例如下方範例設定的 materialButtonStyle 所套用的 style 將會作用在所有 MaterialButton 上,還有 textViewStyle、textColor 也都會套用在所有的 TextView 上
Theme vs Style
他們之間有許多相似的地方,甚至都是用 <style>
去編寫,但他們使用的情境與作用範圍不同。擁有相同的基礎架構,都是將屬性映射到資源的鍵值,得到對應的值,都是透過屬性來定義元件的外觀樣貌
- theme : 能作用於整個 App 或是 ViewGroup
- style : 只能作用於單個 View
Style & Theme 如何作用在各種元件上
在使用之前,我們應該要先了解,這些設置是如何作用在元件上面
Style
只會作用在單一個 view 上面,且 view 只能應用一種 style
應用於 View 的 Style 僅適用於該 View,而不適用於其任何子視圖。例如,如果有一個包含三個按鈕的 ViewGroup,則在 ViewGroup 上設置 Style 不會將該樣式應用於按鈕
Style 提供的值與佈局中直接設置的值有衝突(必須解決層級的順序如下圖 )
Style hierarchy
通常我們都會透過元件自身的 attributes 來改變外觀,或是直接套用 style 或 theme 來統一規格,那如果這些同時應用的話會怎樣?
元件屬性可以通過多種方式方式設置,如果不同方式同時設置必然會引起衝突,比如我在代碼通過 TextView.setTextSize() 設置了字體大小,同時我給此 TextView 設置的 Style 也加了 textSize 的配置,那最終會使用誰定義的 textSize 呢,答案是 setTextSize()。所以這裡就有一個優先級的問題
系統默認會按如下優先級應用,優先級從高到低:
- 通過 Span 來設置TextView及TextView的子類控件屬性
- Java 或 Kotlin 編碼來設置屬性
- setAttributes 方法來設置屬性
- 自定義 Style 來設置屬性
- 默認的 Style 屬性
- 通過 Theme 設置批量 View 的屬性
- 特定 View 的特定 Style 屬性,如通過 TextAppearance 個別為TextView 設置相關屬性
簡單來說,請盡可能使用主題和樣式,以維持一致性
而我們對 View 做單一行為的設定與變化,層級都會是最高的。而 Style 與 theme 就像是一個普遍的規則,是要先遵守的,但在每個元件中透過 attributes 容許我們做一定程度上的改變
藉由這樣的層級關係,讓元件在整體 App 一定程度上,擁有相似的外觀風格
Note : 如果你在項目中設置的 style 修改一直不生效,此時就需要考慮是否其他地方也設置了相關屬性,把 style 中的屬性給覆蓋了
Theme
Theme 是種資源的集合,可以由 style、layout 等引用。它們為 Android 資源提供語義名稱,以便之後可以引用它們,例如 colorPrimary 是給定顏色的語義名稱
這些命名資源被稱為 theme attributes,所以一個 theme 是 Map<theme attribute, resource>。主題屬性與視圖屬性不同,因為它們不是特定於單個視圖類型的屬性,而是語義命名的指向值的指針,這些值更廣泛地適用於應用程序。主題為這些命名資源提供了具體值。通過對資源進行主題抽象,我們可以在不同的主題中提供不同的具體值(如 colorPrimary = orange )
例如我們在 theme 當中設置的 colorPrimacy、colorOnPrimacy、colorSecondary、colorOnSecondary 後,可以在元件中去 reference,只要使用 ?attr/ 的前綴字樣
一但我們去改動了 colorPrimary,所有參照它的元件也會跟變得,這使得我們能透過 theme attributes 一次控制多個元件的屬性
這樣的使用方式,就像是我們讓屬性參照一個數值一樣,讓他們之間擁有了連動性,theme 就是透過這樣的機制去控制 App 所有的元件外觀
theme 是命名資源的集合,在應用程序中廣泛使用
Theme 就像 interface,定義了物件的型態,而 theme 定義了元件的型態。用介面進行編程允許將公共合約與實現分離,從而允許使用不同的實現。主題扮演著類似的角色;通過針對主題屬性編寫佈局和樣式,我們可以在不同的主題下使用它們,提供不同的具體資源
ColorPrimacy、ColorSecondary 是啥?
在上述介紹完 theme 後,大概知道 theme 藉由這些屬性套用在所有的元件上,但這些屬性間的差別在哪?尤其是上述看到的 color 的部分,到底那些屬性是作用於哪些元件中,以下列表帶大家認識一下
?attr/colorPrimary :
應用程序的主要顏色 ( toolbar、button )?attr/colorSecondary :
應用程序的次要顏色,通常是主要品牌顏色的明亮補充 ( FABs )?attr/colorOn[Primary, Secondary, Surface etc] :
與指定顏色形成對比的顏色?attr/color[Primary, Secondary] Variant :
給定顏色的替代色度?attr/colorSurface :
組件表面的顏色,( Card 、Sheet 和 Menu )?android:attr/colorBackground :
屏幕的背景?attr/colorPrimarySurfacecolorPrimarycolorSurface
在 Light 主題 和 Dark 主題之間切換?attr/colorError
用於顯示錯誤的顏色?attr/colorControlNormal
在正常狀態下應用於圖標/控件的顏色?attr/colorControlActivated
應用於處於激活狀態(例如選中)的圖標/控件的顏色?attr/colorControlHighlight
應用於控制高光的顏色(例如波紋、列表選擇器)?android:attr/textColorPrimary
最突出的文本顏色?android:attr/textColorSecondary
輔助文本顏色
如果覺得閱讀這些很吃力,在 Material Design 中也有提供 工具 讓我們快速 demo 這些顏色作用的區域
或是來看一下實際元件如何反應這些屬性配置,這邊以 Button 作為範例。可以看到 Button 的配置都是吃 Primary 的 color,所以一旦我們改動這些 theme attributes,原本 reference 的值就會改變
客製化專屬的元件 Style
想要自定義一個風格屬性,可以在 res/value 去建立資源檔
<style>
: 定義每個風格的名稱<item>
: 定義每個風格的屬性,每個項目中的名稱指定一個屬性,或是在XML當中去設定屬性。<item>
元素中的值是該屬性的值
上方簡單定義了一個 TextView 風格的資源檔案,將顏色與字型都設定了,之後就可以在元件中套用
但是,通常不會將 style 應用於單個視圖,而是將 style 用作整個應用程序、活動或視圖集合的 theme
Extend and customize Style
創建自己的 Style 時,應該從框架或支持庫中擴展現有的 Style,以便保持與平台 UI 樣式的兼容性。拿我們上方的案例來說,設置 TextView 應該去繼承原生的 style,透過繼承的方式,在一些細節上修改。就可以透過擴展的方式,來自訂我們要的風格樣貌,就如同我們在撰寫 class 一樣
除了這種擴展方式之外。也可從自己設計的項目中繼承。可以通過使用點符號擴展樣式名稱來繼承樣式(來自平台的樣式除外),而不是使用 parent 屬性。也就是在樣式名前加上想繼承的樣式名,用句點隔開。通常只有在擴展自己的樣式時才應該這樣做,而不是從其他庫中擴展樣式
Note : 如果使用點符號來擴展樣式,並且還包含 parent 屬性,則父樣式會覆蓋通過點符號繼承的任何樣式
Style Architecture
上述我們知道了擴展的應用方式,所以在 Style 設計命名上是非常重要的,這邊看一下預設的 Style 如何編寫
style="@style/Widget.MaterialComponents.Button.TextButton"
可以看到一開始的 Widget 代表此 style 應用於 App 組件中,MaterialComponents 代表著是屬於 Material Design 設計風格的組件,之後的 Button 就代表著是應用於按鈕上的, TextButton 就是衍生自 Button 概念之下的子型態,是有著 Text 樣貌的 Button
所以在命名規則上,就能依照官方給我們的架構下去設想,順著這樣的邏輯下去設計 Style architecture,也方便我們日後去維護與擴展 (OCP)
上述的範例中,我將各種 Button 的 style 用擴展的方式層層向下延伸。首先建立一個最基底的 Button Style,它會繼承 MaterialButton 的 style。這代表我會沿著 Material Design 的設計方式延伸出我自己的風格,所以在一些基本的屬性配置上,Material Design 的 style 都幫我們做好了
我們就能開始任意透過擴展的方式創造想要的 style,這邊再向下建立了一個 Outlined 形狀的 Style,再擴展出分別有不同文字大小的 Outlined Button。這樣就是算是一個基本的 style architecture,至於分層命名的概念,可以參考官方的設計方式
ThemeOverlay
上面我們簡單示範了如何設計 style 的過程,但還有一個重要的東西沒提到,ThemeOverlay
Theme 的強大之處在於作用的範圍,像是一個 Tree,從 Activity 向下延伸到 ViewGroup、View 等等。在這 Tree 的任何級別上指定 theme 都會連動到後面的節點。例如,將 ViewGroup 設置為某一個 theme ,那麼它將作用於該 ViewGroup 下面所有 Views( 而 style 將只會作用於 單個 view )
theme
依照不同的情境,設計各種不同的 theme,例如夜間模式
layout
接著套用在不同的 ViewGroup,會發現當中的元件會依照 theme 變化
在此 Tree 中的任何級別上設置 theme 都不會替換當前的 theme,而是將其覆蓋,也就是 ThemeOverlay。如下圖的範例中,之後設置的 theme 也繼承了原本的設置,並只有覆蓋了原本的屬性
ThemeOverlay in style
而相同的概念,也能用在我們設計 style 當中,在官方 Material Design 設計 style 中也很常應用到,是希望我們遵從 theme 的架構下更優雅流暢的去設計元件。因為在我們不斷地擴展的當下,也許希望能有不同的顏色風格,但一個一個改又太花時間,這時候使用 materialThemeOverlay
,撰寫一個專屬於這個 style 的配色 theme
雖然 style 已經很方便,但每個元件設置顏色的屬性都不相同,要我們全部記住是不太可能。而透過 theme 就能應用到所有的元件上,這邊直接給大家看著例子
就用上述的 Button style 的範例來改寫,撰寫 materialThemeOverlay
代表我能在這 style 去覆蓋原有的 theme 的相關顏色屬性,因此擴展出各種不同配色的 Button
應用在剛剛上述示範的 Button 中,在 variant theme 去改變其中一個 button
可以看到原先套用的 theme 被覆蓋了
這樣的方式雖然要多寫一個 ThemeOverlay,但至少能確保元件能依照系統配置的方式去改變,不會有在 style 中因為不知道屬性是要用 android: 或 app: 的層級影響,而無法變動想要的屬性等等。
而且在許多元件,是沒有對應的 attributes 或 method 讓我們能直接改動的,但是都一定會依照 theme 的樹狀結構下延伸改變,所以設計 theme 比起去記那些元件屬性名字在進行各種微調來得更有效率
總結
Style 與 Theme 可以說是 Android 為了實現 Material Design 並提升 App UI 設計一致性與整體性的工具與架構,透過它們可以讓開發者有意識的知道,要層次的去設計元件的各種風格,而不只是單純將每個 attributes 包裹在 style 中給元件套用
雖然在使用之前,必須要閱讀它的使用手冊,要知道 colorPrimacy、onPrimay、Secondary、onSecondary、Surface、onSurface 等等這些屬性配置在元件上的規則;要知道透過擴展的方式來設計 style 與 theme;要知道 ThemeOverlay 能彈性的覆蓋原本設置的 theme
但使用 style 與 theme 去配置 View 的屬性,不但能將所有設置的屬性都從單一的 View 中抽離並重複利用,還能透過 style 擴展的方式一層一層的去設置屬性。從最基本的樣式到客製化的形式,都不會破壞原本的設計,也不用再從頭與重複編寫
就像是 interface 一樣,規範了各種屬性,一但有元件去套用繼承它,就會依照規則呈現
一旦上手了,就能更加快速與簡潔地設置 UI 屬性
若是對 android 元件相關的技術有興趣,可以觀看我的鐵人賽文章