JetPack Compose主题配色太少怎么办,来设计自己的颜色系统吧(上)

简介: JetPack Compose 正式版已经发布好几个月了,在这段时间里,除了业务相关需求之外,我也开始了 Compose 在实际项目中的落地实验,因为一旦要接入当前项目,那么遇到的问题其实远远大于新创建一个项目所需要的问题。

引言

JetPack Compose 正式版已经发布好几个月了,在这段时间里,除了业务相关需求之外,我也开始了 Compose 在实际项目中的落地实验,因为一旦要接入当前项目,那么遇到的问题其实远远大于新创建一个项目所需要的问题。

本篇要解决的就是 Compose 默认 Material 主题颜色太少,如何配置自己的业务颜色板,或者说,如何自定义自己的颜色系统,并由点入深,系统的分析相关实现方法与原理。

问题

在开始之前,我们先看看目前创建一个 Compose 项目,默认的 Material 主题为我们提供的颜色有哪些:

网络异常,图片无法展示
|

对于一个普通的应用而言,默认的已基本满足开发使用,基本的主题配色已经足够。但是此时一个问题出现了,如果我存在其他的主题配色呢?

传统做法

在传统的 View 体系中,我们一般都会将颜色定义在 color.xml 文件中,在使用的时候直接读取即可,getColor(R.xx) ,这个大家都已经很熟悉了,那么在 Compose 中呢?

网络异常,图片无法展示
|

Compose

Compose 中,google 将颜色数值统一放在了 theme 下的 color.kt 中,这其实也就是全局静态变量,乍一看好像没什么问题,那我的业务颜色放在那里呢,总不能都全局暴露吧?

但是聪明的你肯定知道,我按照老办法放到 color.xml 里不就行哈,这样也不是不可以,但是随之而来的问题如下:

  • 切换主题时候,颜色怎么统一解决?
  • Googlesimple 里,color.xml 里往往不会写任何配置,即 Google 本身不建议在 compose里这样用

那么我该怎么办,我去看看google的simple,看看他们怎么解决:

网络异常,图片无法展示
|

simple果然是simple 😑 ,Google 完全按照 Material 的标准,即不会出现其他的非主题配色,那实际呢,我们开发怎么办。然后我搜了下目前github上大佬们写的一些开源项目,发现他们也都是按照 Material 去实现,但是很明显这样很不符合实际。🙃

解决思路

随心所欲写法(不推荐)

既然官方没写怎么解决,那就自己想办法解决喽。

compose 中,对于数据的改变监听是使用 MutableState ,那么我自己自定义一个单例持有类,持有现有的主题配置,然后定义一个业务颜色类,并且定义相应的主题颜色类对象,最终根据当前单例的主题配置,去判断最终使用什么颜色板即可,更改业务主题时只需要更改这个单例主题配置字段即可。一想到如此简单,我可真是个抖机灵,说干就干 👨‍🔧‍

创建主题枚举
enum class ColorPallet {
    // 默认就给两个颜色,根据需求可以定义多个
    DARK, LIGHT
}
增加主题配置单例
object ColorsManager {
    /** 使用一个变量维护当前主题 */
    var pallet by mutableStateOf(ColorPallet.LIGHT)
}
增加颜色板
/** 共用的颜色 */
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
/** 业务颜色配置,如果需要增加其他主题业务色,直接定义相应的下述对象即可,如果某个颜色共用,则增加默认值 */
open class CkColor(val homeBackColor: Color, val homeTitleTvColor: Color = Color.Gray)
/** 提前定义好的业务颜色模板对象 */
private val CkDarkColor = CkColor(
    homeBackColor = Color.Black
)
private val CkLightColor = CkColor(
    homeBackColor = Color.White
)
/** 默认的颜色配置,即Md的默认配置颜色 */
private val DarkColorPalette = darkColors(
    primary = Purple200,
    primaryVariant = Purple700,
    secondary = Teal200
)
private val LightColorPalette = lightColors(
    primary = Purple500,
    primaryVariant = Purple700,
    secondary = Teal200
)
增加统一调用入口

为了便于实际使用,我们增加一个 MaterialTheme.ckColor的扩展函数,以便使用我们自定义的颜色组:

/** 增加扩展 */
val MaterialTheme.ckColor: CkColor
    get() = when (ColorsManager.pallet) {
        ColorPallet.DARK -> CkDarkColor
        ColorPallet.LIGHT -> CkLightColor
    }
最终的主题如下
@Composable
fun CkTheme(
    pallet: ColorPallet = ColorsManager.pallet,
    content: @Composable() () -> Unit
) {
    val colors = when (pallet) {
        ColorPallet.DARK -> DarkColorPalette
        ColorPallet.LIGHT -> LightColorPalette
    }
    MaterialTheme(
        colors = colors,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}

效果图

网络异常,图片无法展示
|
 
网络异常,图片无法展示
|

看效果也还成,简单粗暴,[看着] 也没什么问题,那有没有什么其他方式呢?我还是不相信官方没有写,可能是我疏忽了。

自定义颜色系统(官方)

就在我翻官方文档时,突然看见了这样几个小字,它实现了自定义颜色系统

网络异常,图片无法展示
|

增加颜色模板
// 示例,正确做法是放到color.kt下
val Blue50 = Color(0xFFE3F2FD)
val Blue200 = Color(0xFF90CAF9)
val A900 = Color(0xFF0D47A1)
/**
 * 实际主题的颜色集,所有颜色都需要添加到其中,并使用相应的子类覆盖颜色。
 * 每一次的更改都需要将颜色配置在下方 [CkColors] 中,并同步 [CkDarkColor] 与 [CkLightColor]
 * */
@Stable
class CkColors(
    homeBackColor: Color,
    homeTitleTvColor: Color
) {
    var homeBackColor by mutableStateOf(homeBackColor)
        private set
    var homeTitleTvColor by mutableStateOf(homeTitleTvColor)
        private set
    fun update(colors: CkColors) {
        this.homeBackColor = colors.homeBackColor
        this.homeTitleTvColor = colors.homeTitleTvColor
    }
    fun copy() = CkColors(homeBackColor, homeTitleTvColor)
}
/** 提前定义好的颜色模板对象 */
private val CkDarkColors = CkColors(
    homeBackColor = A900,
    homeTitleTvColor = Blue50,
)
private val CkLightColors = CkColors(
    homeBackColor = Blue200,
    homeTitleTvColor = Color.White,
)
增加 xxLocalProvider
@Composable
fun ProvideLcColors(colors: CkColors, content: @Composable () -> Unit) {
    val colorPalette = remember {
        colors.copy()
    }
    colorPalette.update(colors)
    CompositionLocalProvider(LocalLcColors provides colorPalette, content = content)
}
增加 LocalLcColors 静态变量
// 创建静态 CompositionLocal ,通常情况下主题改动不会很频繁
private val LocalLcColors = staticCompositionLocalOf {
    CkLightColors
}
增加主题配置单例
enum class StylePallet {
    // 默认就给两个颜色,根据需求可以定义多个
    DARK, LIGHT
}
/* 针对当前主题配置颜色板扩展属性 */
private val StylePallet.colors: Pair<Colors, CkColors>
    get() = when (this) {
        StylePallet.DARK -> DarkColorPalette to CkDarkColors
        StylePallet.LIGHT -> LightColorPalette to CkLightColors
    }
/** CkX-Compose主题管理者 */
object CkXTheme {
    /** 从CompositionLocal中取出相应的Local */
    val colors: CkColors
        @Composable
        get() = LocalLcColors.current
    /** 使用一个state维护当前主题配置,这里的写法取决于具体业务,如果你使用了深色模式默认配置,则无需这个变量,即app只支持深色与亮色,那么只需要每次读系统配置即可。但是compose本身可以做到快速切换主题,那么维护一个变量是肯定没法避免的 */
    var pallet by mutableStateOf(StylePallet.LIGHT)
}
最终主题代码
@Composable
fun CkXTheme(
    pallet: StylePallet = CkXTheme.pallet,
    content: @Composable () -> Unit
) {
    val (colorPalette, lcColors) = pallet.colors
    ProvideLcColors(colors = lcColors) {
        MaterialTheme(
            colors = colorPalette,
            typography = Typography,
            shapes = Shapes,
            content = content
        )
    }
}

分析

最终的效果和上述的一致,也就不具体赘述了,我们主要来分析一下,为什么Google要这么写:

我们可以看到上述的示例里主要是使用了 CompositionLocalProvider 去保存当前的主题配置 ,而 CompositionLocalProvider 又继承自 CompositionLocal ,比如我们常用的 MaterialTheme 主题中的 Shapestypography 都是由此来管理。

CkColors 这个类上增加了 @Stable ,其代表着对于 Compose 而言,这个类是一个稳定的类,即每次更改不会引发重组,内部的颜色字段使用了 mustbaleStateOf 包装,以便当颜色更改时触发重组,内部还增加了 update()copy() 方法,以便于管理与单向数据的更改。

其实如果我们去看的 Colors 类。就会发现上述示例中的 CkColors 和其是完全一样的设计方式。

所以在Compose中自定义主题颜色,其实就是我们在 Colors 的基础上自己又写了一套自己的配色。😂

既然这样,那为什么我们不直接继承Colors去增加配色呢?使用的时候我强制一下不就行,这样不就不用再自己造什么 CompositionLocal 了?

其实很好理解,因为 Colors 中的 copy() 以及 update() 无法被重写,即没加 open ,而且其内部变量使用了 internal 修饰 set 方法更重要的原因是这样 不符合Md的设计 ,所以这也就是为什么 需要我们去自定义自己的颜色系统,甚至于可以完全自定义自己的主题系统。前提是你觉得自定义的主题里面再包装一层 MaterialTheme 主题比较丑陋的话,当然相应的,你也需要考虑如何解决其他附带的问题。

目录
相关文章
|
16天前
|
存储 缓存 Android开发
安卓Jetpack Compose+Kotlin, 使用ExoPlayer播放多个【远程url】音频,搭配Okhttp库进行下载和缓存,播放完随机播放下一首
这是一个Kotlin项目,使用Jetpack Compose和ExoPlayer框架开发Android应用,功能是播放远程URL音频列表。应用会检查本地缓存,如果文件存在且大小与远程文件一致则使用缓存,否则下载文件并播放。播放完成后或遇到异常,会随机播放下一首音频,并在播放前随机设置播放速度(0.9到1.2倍速)。代码包括ViewModel,负责音频管理和播放逻辑,以及UI层,包含播放和停止按钮。
|
16天前
|
存储 数据库 Android开发
安卓Jetpack Compose+Kotlin,支持从本地添加音频文件到播放列表,支持删除,使用ExoPlayer播放音乐
为了在UI界面添加用于添加和删除本地音乐文件的按钮,以及相关的播放功能,你需要实现以下几个步骤: 1. **集成用户选择本地音乐**:允许用户从设备中选择音乐文件。 2. **创建UI按钮**:在界面中创建添加和删除按钮。 3. **数据库功能**:使用Room数据库来存储音频文件信息。 4. **更新ViewModel**:处理添加、删除和播放音频文件的逻辑。 5. **UI实现**:在UI层支持添加、删除音乐以及播放功能。
|
10天前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
12天前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
12天前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
16天前
|
缓存 Android开发 Kotlin
【安卓app开发】kotlin Jetpack Compose框架 | 先用OKhttp下载远程音频文件再使用ExoPlayer播放
使用 Kotlin 的 Jetpack Compose 开发安卓应用时,可以结合 OkHttp 下载远程音频文件和 ExoPlayer 进行播放。在 `build.gradle` 添加相关依赖后,示例代码展示了如何下载音频并用 ExoPlayer 播放。代码包括添加依赖、下载文件、播放文件及简单的 Compose UI。注意,示例未包含完整错误处理和资源释放,实际应用需补充这些内容。
|
15天前
|
监控 Android开发 数据安全/隐私保护
安卓kotlin JetPack Compose 实现摄像头监控画面变化并录制视频
在这个示例中,开发者正在使用Kotlin和Jetpack Compose构建一个Android应用程序,该程序 能够通过手机后置主摄像头录制视频、检测画面差异、实时预览并将视频上传至FTP服务器的Android应用
|
13天前
|
安全 网络安全 API
kotlin安卓开发JetPack Compose 如何使用webview 打开网页时给webview注入cookie
在Jetpack Compose中使用WebView需借助AndroidView。要注入Cookie,首先在`build.gradle`添加WebView依赖,如`androidx.webkit:webkit:1.4.0`。接着创建自定义`ComposableWebView`,通过`CookieManager`设置接受第三方Cookie并注入Cookie字符串。最后在Compose界面使用这个自定义组件加载URL。注意Android 9及以上版本可能需要在网络安全配置中允许第三方Cookie。
|
15天前
|
Android开发 Kotlin
kotlin安卓开发【Jetpack Compose】:封装SnackBarUtil工具类方便使用
GPT-4o 是一个非常智能的模型,比当前的通义千问最新版本在能力上有显著提升。作者让GPT开发一段代码,功能为在 Kotlin 中使用 Jetpack Compose 框架封装一个 Snackbar 工具类,方便调用
|
16天前
|
Android开发 Kotlin
安卓Jetpack Compose+Kotlin, 使用ExoPlayer播放多个【本地】音频,播放完随机播放下一首,遇到播放错误,也自动播放下一首
使用Kotlin和Jetpack Compose开发的安卓应用中,实现了两个EvoPlayer同时播放res/raw目录下的音频。一个音轨播放人声(顺序播放),另一个播放背景音乐(随机播放)。每个音轨都有独立的播放和停止控制,且在播放结束或遇到错误时会自动切换到下一首。MediaPlayer置于ViewModel中,UI界面包含播放和停止按钮,控制两个音轨。每次切换音频前,还会随机调整播放速度在0.9到1.2之间。代码示例展示了如何创建ViewModel和UI以实现这一功能。