開(kāi)端
應(yīng)用換膚,這真的是一個(gè)老生常談的問(wèn)題豌拙,從原生安卓開(kāi)始陕悬、到后來(lái)的 Flutter
,再到現(xiàn)在的 Compose
按傅,雖說(shuō)老生常談捉超,但其實(shí)還是新瓶裝舊酒。
安卓原生的主題切換這里不再說(shuō)了逞敷,這不是本文的重點(diǎn),況且那個(gè)一篇文章估計(jì)也說(shuō)不清??灌侣。
Flutter
的主題切換主要依賴(lài)于 provider
狀態(tài)管理推捐,其實(shí)在 Compose
中也差不多,且聽(tīng)完娓娓道來(lái)侧啼!
GitHub 地址在文章末尾牛柒。先來(lái)看看實(shí)現(xiàn)效果吧:
經(jīng)過(guò)
其實(shí) Compose
雖說(shuō)換膚實(shí)現(xiàn)很簡(jiǎn)單,但是這也需要在你遵守 Compose
開(kāi)發(fā)規(guī)范的前提下痊乾,比如定義顏色的時(shí)候不使用硬編碼皮壁,而使用 MaterialTheme
中的顏色,當(dāng)然還有 Shape
哪审、Typography
等蛾魄。
Compose
中的主題大家應(yīng)該都很熟悉了,但應(yīng)該還有不是很熟悉的,這里先來(lái)簡(jiǎn)單說(shuō)下吧滴须。
Compose 中的主題
當(dāng)你創(chuàng)建一個(gè)新的 Compose
項(xiàng)目之后舌狗,系統(tǒng)會(huì)自動(dòng)幫你創(chuàng)建下面的結(jié)構(gòu):
可以看到系統(tǒng)一共創(chuàng)建了四個(gè)文件,顧名思義扔水,分別為:顏色痛侍、形狀、主題魔市、類(lèi)型主届,本文咱們主要看顏色及主題。
先來(lái)看看 Color 文件默認(rèn)是什么樣子的吧:
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
上面就是 Color 文件中的代碼待德,只是簡(jiǎn)單定義了四種顏色君丁。
下面再來(lái)看看 Theme 文件吧:
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
)
@Composable
fun PlayAndroidTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
可以看到 Theme 中先是定義了深色和淺色兩個(gè)顏色,然后通過(guò)判斷是否為夜間模式來(lái)動(dòng)態(tài)適配磅网。
下面來(lái)看看 lightColors 方法吧:
fun lightColors(
primary: Color = Color(0xFF6200EE),
primaryVariant: Color = Color(0xFF3700B3),
secondary: Color = Color(0xFF03DAC6),
secondaryVariant: Color = Color(0xFF018786),
background: Color = Color.White,
surface: Color = Color.White,
error: Color = Color(0xFFB00020),
onPrimary: Color = Color.White,
onSecondary: Color = Color.Black,
onBackground: Color = Color.Black,
onSurface: Color = Color.Black,
onError: Color = Color.White
): Colors
通過(guò)上面代碼可以得知谈截,可以在這里設(shè)置每種顏色值,深淺模式都類(lèi)似涧偷,都可以進(jìn)行設(shè)置簸喂。
使用主題
上面簡(jiǎn)單說(shuō)了下 Compose
中的主題,那么主題寫(xiě)好之后應(yīng)該如何進(jìn)行使用呢燎潮?來(lái)看代碼:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// 使用主題
PlayAndroidTheme {
NavGraph()
}
}
}
沒(méi)錯(cuò)喻鳄,就這么簡(jiǎn)單,只需要在頁(yè)面外層嵌套下剛才設(shè)置的主題即可∪贩猓現(xiàn)在主題是設(shè)置上了除呵,那應(yīng)該如何使用剛才設(shè)置到主題中的那些顏色呢?亦或是別的資源爪喘?上代碼:
// 顏色使用
MaterialTheme.colors.primary
// 形狀使用
MaterialTheme.shapes.large
// 字體類(lèi)型使用
MaterialTheme.typography.body1
還記得上面所說(shuō)的需要遵守 Compose
的開(kāi)發(fā)規(guī)范吧颜曾,所說(shuō)的其實(shí)就是使用這些資源的時(shí)候盡量使用咱們定義好的。
解決
如何切換主題
首先需要思考如何來(lái)進(jìn)行主題的切換秉剑,整個(gè)主題肯定使用在項(xiàng)目的開(kāi)始——啟動(dòng) Activity 中泛豪,但切換主題的頁(yè)面肯定不在一塊,那這個(gè)時(shí)候應(yīng)該如何在切換主題頁(yè)面切換了之后讓 Activity 知道呢侦鹏?
最開(kāi)始的時(shí)候我的想法還是不夠 Compose
诡曙,我想的是使用廣播,在切換主題頁(yè)面點(diǎn)擊之后發(fā)送一個(gè)廣播略水,然后在 Activity 中進(jìn)行接收价卤,然后接收到之后刷新。我確實(shí)這樣做了渊涝,功能也確實(shí)實(shí)現(xiàn)了慎璧,但是總感覺(jué)哪里不對(duì)床嫌,感覺(jué) Compose
中不應(yīng)該這樣才對(duì)。
中午在食堂吃飯的時(shí)候突然想到:Compose
中全部都是以狀態(tài)驅(qū)動(dòng) UI 改變的炸卑,我直接將主題切換設(shè)置成一個(gè)狀態(tài)不得了既鞠!
開(kāi)搞
說(shuō)干就干,首先先來(lái)設(shè)置下咱們默認(rèn)的幾套主題吧:
// 天藍(lán)色
const val SKY_BLUE_THEME = 0
// 灰色
const val GRAY_THEME = 1
// 深藍(lán)色
const val DEEP_BLUE_THEME = 2
// 綠色
const val GREEN_THEME = 3
// 紫色
const val PURPLE_THEME = 4
// 橘黃色
const val ORANGE_THEME = 5
// 棕色
const val BROWN_THEME = 6
// 紅色
const val RED_THEME = 7
// 青色
const val CYAN_THEME = 8
// 品紅色
const val MAGENTA_THEME = 9
這里為了之后代碼簡(jiǎn)單點(diǎn)沒(méi)有使用枚舉盖文,其實(shí)都差不多的嘱蛋。
然后添加一個(gè)主題的狀態(tài):
/**
* 主題狀態(tài)
*/
val themeTypeState: MutableState<Int> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
mutableStateOf(getDefaultThemeId())
}
可以看到主題狀態(tài)中有一個(gè)名為 getDefaultThemeId 的方法,用來(lái)獲取默認(rèn)主題 ID五续,來(lái)看下吧:
/**
* 獲取當(dāng)前默認(rèn)主題
*/
fun getDefaultThemeId(): Int = DataStoreUtils.getSyncData(CHANGED_THEME, SKY_BLUE_THEME)
這里使用了 DataStore
來(lái)進(jìn)行數(shù)據(jù)的存取洒敏,DataStore
也是 Jetpack
中的一員,感興趣的可以看看我之前寫(xiě)的文章:再抱一抱DataStore
然后修改下主題方法:
@Composable
fun PlayAndroidTheme(
themeId: Int = 0,
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
playDarkColors()
} else {
getThemeForThemeId(themeId)
}
MaterialTheme(
colors = colors,
typography = typography,
shapes = Shapes,
content = content
)
}
可以看到上面添加了一個(gè)參數(shù) themeId 用來(lái)表示當(dāng)前需要使用的主題 id疙驾,getThemeForThemeId 方法用來(lái)通過(guò)主題 ID 來(lái)獲取需要的主題顏色集凶伙,來(lái)看下實(shí)現(xiàn)吧:
/**
* 通過(guò)主題 ID 來(lái)獲取需要的主題
*/
private fun getThemeForThemeId(themeId: Int) = when (themeId) {
SKY_BLUE_THEME -> {
playLightColors(
primary = primaryLight
)
}
GRAY_THEME -> {
playLightColors(
primary = gray_theme
)
}
// 別的主題類(lèi)似,篇幅原因省略
}
接下來(lái)修改下 Activity 中的調(diào)用吧:
PlayAndroidTheme(themeTypeState.value) {
NavGraph()
}
由于 themeTypeState 是一個(gè) State它碎,所以當(dāng)它的值改變的時(shí)候函荣,就會(huì)自動(dòng)刷新 UI。
收尾
創(chuàng)建主題列表
先做下準(zhǔn)備工作扳肛,先來(lái)創(chuàng)建一個(gè)用來(lái)存放主題信息的實(shí)體類(lèi)吧:
data class ThemeModel(val color: Color, val colorId: Int, val colorName: String)
接下來(lái)將咱們添加的主題放到一個(gè) List 中:
// 主題model列表
private val themeList = arrayListOf(
ThemeModel(primaryLight, SKY_BLUE_THEME, "天藍(lán)色"),
ThemeModel(gray_theme, GRAY_THEME, "灰色"),
ThemeModel(deep_blue_theme, DEEP_BLUE_THEME, "深藍(lán)色"),
ThemeModel(green_theme, GREEN_THEME, "綠色"),
ThemeModel(purple_theme, PURPLE_THEME, "紫色"),
ThemeModel(orange_theme, ORANGE_THEME, "橘黃色"),
ThemeModel(brown_theme, BROWN_THEME, "棕色"),
ThemeModel(red_theme, RED_THEME, "紅色"),
ThemeModel(cyan_theme, CYAN_THEME, "青色"),
ThemeModel(magenta_theme, MAGENTA_THEME, "品紅色"),
)
創(chuàng)建主題切換頁(yè)面
萬(wàn)事具備傻挂,只欠東風(fēng)。現(xiàn)在主題這塊已經(jīng)全部準(zhǔn)備好了挖息,只需要再創(chuàng)建一個(gè)主題切換的頁(yè)面金拒,點(diǎn)擊的時(shí)候保存下來(lái)主題 ID 并刷新下 themeTypeState 的值即可。
由上圖可以看出這個(gè)布局最好使用 LazyVerticalGrid套腹,然后設(shè)置下一行放 5 個(gè) item 即可:
LazyVerticalGrid(
cells = GridCells.Fixed(5),
modifier = Modifier.padding(horizontal = 10.dp)
) {
items(themeList) { item: ThemeModel ->
ThemeItem(playTheme, item) {
val playTheme = item.colorId
themeTypeState.value = playTheme
DataStoreUtils.putSyncData(CHANGED_THEME, playTheme)
}
}
}
OK了绪抛,結(jié)束了,這樣就可以了电禀,已經(jīng)可以實(shí)現(xiàn)文章開(kāi)頭 Gif 圖的那種效果了幢码。
結(jié)局
這么快就到結(jié)局了,還有點(diǎn)不舍尖飞,哈哈哈症副。光顧著寫(xiě)實(shí)現(xiàn)了,還沒(méi)放 Github 地址呢葫松,在這里:https://github.com/zhujiang521/PlayAndroid
別忘了切換分支:compose
上面對(duì)主題的講解只是一帶而過(guò)瓦糕,如果大家想系統(tǒng)地學(xué)習(xí) Compose
的話(huà)底洗,可以購(gòu)買(mǎi)我的新書(shū)《Jetpack Compose:Android全新UI編程》進(jìn)行閱讀腋么,里面有完整的 Compose 框架供大家學(xué)習(xí)。
如果對(duì)你有幫助的話(huà)亥揖,別忘記點(diǎn)個(gè) Star珊擂,感激不盡圣勒。
其實(shí)還有一些細(xì)節(jié)的東西我沒(méi)有說(shuō)到,大家如果有疑問(wèn)的話(huà)可以在評(píng)論區(qū)提出來(lái)摧扇。
好了圣贸,先寫(xiě)到這里吧,再會(huì)扛稽!