App爲了漂亮臉蛋也要美顏,Theme 與 Style 的使用,附一鍵變裝 demo

前言

作爲 Android 開發者,不知你是否也有這樣的體驗,隨着項目變得越來越大,各種不同圓角的 shape,不同透明度的 color,不同大小的陰影效果,它們使資源文件越來越多

我認爲造成這種問題的原因有兩個:一個是產品設計的不規範,整個 app 沒有統一的設計風格;第二個便是開發者在開發過程中編碼的不規範

Android Dev Summit '19 有一場關於 Style 與 Theme 的演講,它的 中文字幕視頻在這裏

我爲你整理了每個主題所在的位置

時間 內容
02:14 Styling vs Theme
08:55 Theme Overlay
12:36 Color
17:35 使用及三個技巧
24:00 Material 顏色
28:06 Material 排版
30:07 Material 形狀
34:41 Dark Theme

在視頻下方的評論區,點擊相應時間即可跳轉到指定內容

視頻下方評論區

與之對應的有一個 Styling 的系列文章,我最近翻譯成了中文

本文整理了視頻與文章中的內容,介紹在開發過程中,我們應如何利用 theme 與 style 更優雅地管理資源文件,並提供了很多實用的技巧,在標題中找到技巧相關的查看即可

並且提供了演示 demo,效果如下

demo

 

理解 Style 與 Theme 的區別

這部分內容在視頻的 02.14 處

Style 是 View 屬性的集合,可以將 Style 視爲 Map<View Attribute, Resource>,其中 key 爲 View 的屬性,value 爲資源

sytle key 爲 View Attributes

style value 爲 Resources

Resource 可以爲以下類型

而 Theme 則不同,它的 key 是 「主題屬性」,很顯然下圖中的 colorPrimary 不是任何 View 中的屬性

主題的定義

主題屬性有點像把配置抽象爲語義化的命名的變量,並把它們塞到 map 中,以便未來使用,主題屬性 與 View 屬性很像,它們在 attr 中定義的方式以及對應的類型都是類似的,但二者仍有差異

主題屬性的定義

在引用主題屬性時,可以使用 ?.attr 語法,其中 ?代表在當前主題中搜索

主題屬性帶來的優勢

如果我們 app 需要支持普通版本和 Pro 版本,它們的主色不同,我們只需定義兩個主題,配置不同的 colorPrimary。接着我們需要適配深色主題,那麼只需提供不同的數值即可

values文件夾下主題配置

values-night文件夾下主題配置

這就好比我們有一個 Theme 抽象類,而其中有一個抽象屬性 colorPrimary,它有四個實現類,分別重寫了 colorPrimary 屬性,這樣我們便得到了 四個變體,未來想加入新的變體,只需繼承該抽象類並重寫屬性即可。看過 第一篇譯文 的小夥伴知道,主題的作用範圍是 「樹」中的所有子節點。這樣我們便很輕鬆地實現了更改程序主色的功能

如果要使用 Style 實現這一功能,首先,我們需要定義四種 Style。由於 Style 的作用範圍是特定 View,因此我們要爲每個 View 均定義四套 Style

兩種方案對比

小結

簡單來說,Style 與 Theme 的作用範圍不同

Style 只會作用於單一的 View 中,使用時用 style 標籤

Style 作用範圍

Theme 會作用於「樹」中的所有子節點,使用時用 theme 標籤

theme 作用範圍

在任意時刻,程序都是運行在某一特定主題下的,例如 activity 被設置了特定主題

我們在使用時應該注意 theme 與 style 各自的優勢,靈活運用二者

Theme Overlay

這部分內容在視頻的 08.55 處

主題是有繼承關係的,當該繼承關係鏈中有多個主題配置了同一屬性,那麼最繼承鏈最底部的內容會生效,在下圖中,如果多個主題都聲明瞭 colorPrimary,那麼 Theme.Owl.Pink 中的內容會生效(這有點像 Java 的繼承關係)

利用這種繼承關係我們可以實現在粉色主題下將部分界面使用藍色主題

粉色主題下,某部分需要一個藍色主題

我們看一下兩種主題的繼承關係,這兩種主題的父級應該是比較相似的,這看起來比較浪費,因爲很多屬性是相同的

另外,當你把一個主題設置在另一個主題之上,你需要注意不能將自己想要保留的東西被覆蓋掉

Theme Overlay 可以很好地解決這一問題,它並不是一項新技術,而是屬於一種技巧

Theme Overlay

接下來我們只需關注想要更改的東西,應用下圖的聲明的主題,則會只改變 colorPrimary 和 colorSecondary 兩項屬性的值,而其它的所有屬性均不變

技巧1:反轉顏色

MaterialComponents 提供了 暗色的 Theme Overlay

使用該 Theme Overlay 可以將淺色主題中的某部分做成暗色主題

技巧2:使用正確的 Context

我們知道主題與 Context 相關,由於上文我們提到的主題的繼承關係,使用正確的 Context 很重要

記住:使用距離最近 View 所在的 Context

當然更好的做法是使用主題屬性

技巧3:在代碼中使用 Theme Overlay

如果想要在代碼中使用 Theme Overlay ,可以將其包裹爲 ContextThemeWrapper,這也是 android:theme 標籤內部做的事情

使用 Theme 和 Style

Color

這部分內容在視頻的 12.36 處

程序內最重要的資源便是 Color,Android 定義 Color 有很多種方式,比如 Color tag,它主要由 ARGB 色值組成

 

Color tag 也可以引用其他的 Color tag。需要注意的是,你不能在 Color tag 引用主題屬性

以上是靜態的顏色,下面我們談一談有狀態的顏色

Color state list 允許你在不同狀態下定義不同的顏色,它們也可以用作 drawable,例如下面的例子,在 button 按下或禁用時都有相應的顏色

下面我們來看一下 Color state list 是如何定義的,我們看到這裏使用了 selector 標籤,本示例中有兩個 item,第一項定義了一種顏色,並指定它是在選中狀態下才被使用;第二項 沒有定義任何狀態,這意味着它是默認顏色。如果沒有其他顏色可以匹配當前狀態時,它就會被使用

這裏有一個小技巧,可以將 Color state list 按照最容易出現——最不容易出現的順序進行排序,這由其背後的實現原理決定,系統會遍歷每個 item 直至找出匹配項

在 API 21 官方引入了 android:alpha tag 來設置透明度,並在 API 23 中可以引用主題屬性。如果你使用 AppCompat 的話,它可以向下兼容到 API 14

技巧1:設置透明度

我們在開發中可能會遇到這種情況:對於同一個顏色,我們需要不同的透明度。因此我們可能會複製不同透明度的色值

你可能不想在更改一個顏色後然後再逐一更改其相應透明度的色值,此處我們可以使用 ColorStateList,我們可以使用默認顏色的功能,只配置一個 item,並在此處配置透明度(從 0 到 1)

技巧2:ColorStateList 與 Drawable 的轉換

我們在 View 配置 background 等屬性時可以直接傳入 color,在內部系統會填充該顏色並將其包裝爲 ColorDrawable

 

但如果你將 ColorStateList 傳入是不行的,在 API 28 及之前的設備會崩潰

這是因爲 ColorDrawable 是無狀態的,在 Android 10 中,官方加入了 ColorStateListDrawable 解決了這一問題

爲了在所有 API 中獲得相同的體驗,我們可以使用一種變通的做法,使用 backgroundTint

此處使用純色設置了一個矩形,接着使用 backgroundTint 指向了 ColorStateList

常用技巧

這部分內容在視頻的 17.35 處

技巧1:正確命名資源

我們的項目中肯定有這樣命名的資源,它們是按照主題屬性命名的。Android Studio 新建項目默認的資源就是這樣命名的

而你的主題大概是這樣,主題屬性指向同名的顏色資源

 

這樣做是不推薦的

我們需要的不是一個語義的命名,而是需要一個文字的命名,我們可以用品牌顏色命名,也可以像 Material Color System,根據色調命名

根據品牌命名

根據色調命名

 

技巧2:使用統一的 style 名稱

大家可能見過這樣的樣式,一個叫 AppTheme,一個叫 Toolbar,從命名便可以看出它們的用途

但是如果我們加入了第三種樣式,它的用途不是很明顯,我們無法區分它是一個主題還是樣式

爲此我們可以約定一個命名規則

第一部分爲 Style type:主題,樣式,文本外觀,Theme Overlay,形狀外觀等等

第一部分

第二部分爲 Group name:通常採用應用名稱,如果是多 module 也可以爲 module 名

第二部分

第三部分爲 Sub-group name:這名字通常用於 Widget,也就是使用樣式的 View 的名稱

第三部分

第四部分爲 Variant name:這是可選的,它是主題的變量

第四部分

回到最初的例子,按照我們約定的命名規範改造就變成了這樣

按照命名規範命名

這裏有一個值得注意的地方,在 Android System 中,. 是一個十分神奇的符號,這裏有一個基於它的隱含的繼承系統

上圖的 Widget.MyApp.Toolbar.Blue 實際上繼承了中間的這個主題

這種命名規範可以在 code review 時直觀地判斷出 style 或 theme 是否用錯。如下圖,很明顯這裏使用了 style 標籤,卻傳入了一個主題

你甚至可以使用 Lint 來解決此問題,詳情移步

技巧3:拆分多個文件

簡單模式

將資源類型文件進行標準的分類

  • theme.xml :Theme 和 Theme Overlay
  • type.xml:字體,文本外觀,文本尺寸,字體文件等
  • style.xml:只有 Widget style
  • dimens.xml colors.xml strings.xml:其它類型歸類於實際的資源類型

簡單模式

複雜模式

複雜模式是按照邏輯進行分類,例如形狀相關的放入 shape.xml,如果想要實現全屏的UI,可以在 sys_ui.xml 中控制狀態欄/導航欄顏色,以及是否顯示等等

複雜模式

在 Android Studio 的 Android 視圖下,這樣做的效果是很好的。如下圖,可以很清晰的看到 light 主題和 dark 主題的主題文件

Material

顏色系統

這部分內容在視頻的 24:00 處

該系統構建基礎是大量使用語義命名的變量,這些變量都屬於「主題屬性」。它的運作原理是 library 展示與這些使用語義命名的顏色相關的主題屬性,而開發者負責爲這些顏色提供數值。在 library 內,用這些顏色構建所有的 Widget

對於顏色系統,開發者需要了解一些常用的顏色

colorPrimarycolorSecondary 是 app 品牌的主要顏色,Variant 爲主色的對比色;colorSurface 十分有用,它負責在某些控件表面的顏色;colorError 是錯誤的警示色,因此你沒必要在使用時硬編碼這些顏色

顏色系統還會提供一些 On 命名的顏色,這種顏色會保證擁有和類似名稱顏色形成對比的顏色,例如 colorOnPrimary 永遠會和 colorPrimary 形成對比

你可以在自己的主題中配置這些顏色,注意這裏不必配置所有的顏色,如果你繼承了一些 Material 主題,它們會提供所有顏色的默認色,比如下圖中沒有設置 colorSurface,則會使用 Material Light 主題內定義的 colorSurface

之後你便可以在 layout 或 style 中使用這些顏色了

技巧

一個有用的技巧是可以將這些顏色與 ColorStateList 結合

比如我們想做一個分割線,不必創建名爲 colorDivider 的新顏色,直接從 colorOnSurface 中取一個顏色即可,這個顏色肯定會和背景色形成對比

而且它會響應不同的主題,在淺色主題下,20% colorOnSurface 是一種黑中帶白的顏色。在暗色主題下,colorOnSurface 會變成白色,此時的 20% colorOnSurface 會提供合適的對比

以語義命名的顏色是十分有用的,你可以省去大量的顏色定義

排版系統

這部分內容在視頻的 28:06 處

在設計中,通常使用固定的幾種字號進行排版,例如大標題1,大標題2,文本主體,副標題,按鈕等等

而這些都是作爲主題屬性實現的

然後便可以在應用中引用這些主題屬性

Material 的 Text 十分強大,它可以設置行高,如果你遇到了設置行高卻不生效的問題,使用 Material 組件可以解決這一問題

形狀系統

這部分內容在視頻的 30:07 處

Material 採用了 shape system,該系統爲小型,中型和大型組件 提供 了主題屬性。請注意,如果要在自定義組件上設置 shape,則可能要使用 MaterialShapeDrawable 作爲其背景,它可以理解並實現 shape

這裏是通過 ShapeAppearance 來定義的,ShapeAppearance 和 TextAppearance 類似,是一種針對形狀系統的配置

它由幾個組件組成

首先是 cornerFamily,支持圓角和切角,圓角的方向等等

ShapeAppearance 還支持 overlay,可以更改特定的 Widget

在使用 overlay 時要注意的是很多 Material 組件是有着自己的 ShapeAppearance overlay,例如 BottomSheet,它會取消底部的圓角

技巧

形狀系統是由 MaterialShapeDrawable 實現的

而 MaterialShapeDrawable 有一個強大的功能就是它有着一個屬性叫 interpolation

 

 

使用它可以爲形狀系統做動畫,如果它的值爲0,那麼所配置形狀不會生效,如果值爲1,那麼形狀系統會完整地應用到 drawable 上

暗黑主題

這部分內容在視頻的 34:41 處

暗黑主題的適配很簡單,可以通過代碼設置當前主題和獲取當前主題

 

可以使用 Material 組件的 DayNight,這樣在打開/關閉 暗黑主題時相應的主題屬性的色值都會跟隨變化

技巧1 :抽取主題

很多時候只做上面的兩步並不能很好地適配暗黑主題,例如我們的應用在淺色主題下是這樣的,深色的內容在淺色的背景上

而使用了夜間主題,可能會變成這樣

而我們想要的效果是這樣的

這是由於設置顏色時硬編碼導致的

實際上,這種情況使用主題屬性會有更好的效果

如果想要 colorPrimary 在不同的主題下使用不同的顏色,我們應該如何設置?

或許你會在 values-night/colors.xml 爲暗色主題定義色值,但不建議這樣做!

不建議這樣!

最好的做法是抽取公共部分到基礎主題,然後在此基礎上對淺色和深色主題分別配置差異化的屬性

技巧2:ColorPrimary 的使用

有些時候我們的 colorPrimary 是一種亮色,例如下圖中的藍色,但在暗黑主題下我們想使用相對較暗的顏色,例如 ?attr/colorSurface,Material 組件內部爲我們做好了轉換,直接使用 ?attr/colorPrimarySurface 即可

 

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