学不动了,入门 Compose Styles API

Jetpack Compose 近期推出了全新的 Styles API,用于定制 UI 元素和组件的样式。

在不久的过去(好吧,我不得不承认现在 API 更新太快了),这类事情通常依赖 Modifier 或组件上的 paddingcolor 等参数;Styles API 则把这些样式收拢到统一的 Style 参数或 Modifier.styleable 中。

本文会先整理它的基本概念、使用方式,后续大概会有两三篇文章继续学习该 API,同时也会和大家一起探讨 Styles API 的具体用法。

注意: Styles API 目前处于 @Experimental 阶段,后续版本可能会有变化,Material Design 的样式支持也将在未来版本中提供。

为什么需要 Styles API

Compose 已经有 Modifier 了,为什么还要再引入一个 Style

我相信,不是我一个人第一次看到这个 API 的时候这么想!

在我深入学习且已经应用了一段时间之后,我自己的看法简单来说是:Modifier 仍然负责很多事情,但它不一定适合承载所有"样式配置"。

当组件有大量颜色、间距、形状、字体、状态样式参数时,这些配置会分散在参数、Modifier 和状态判断里,组件 API 也会越来越臃肿。

你想想,如果你做了一个复杂的控件,里面有很多地方可以自定义样式,你会怎么办?

传统的 Modifier 用法更适合控制最外层控件;至于控件内部可定制的部分,我们一般不是继续传递 Modifier,而是通过函数参数暴露出来。

Styles 想解决的,是把这类外观规则集中起来,让组件更容易被主题、设计系统和交互状态统一驱动。

这个问题如果要展开,其实会涉及 Modifier 的职责边界、组件 API 设计、状态样式和设计系统复用,今天这篇文章我们先不展开。

把 Styles API 的基本概念和用法看完,我们再回头讨论"它到底该放在什么位置","它应该怎么用"。

为了我少打字,我后续使用 Styles 或者 Style 来表示 Styles API。

核心概念

Styles 主要解决几类问题:

  • 简化状态样式 :以声明式方式定义基于 hoverfocuspressed 等状态的样式,大幅减少模板代码;
  • 内置动画支持 :状态切换时的样式过渡动画开箱即用,且避免了 animateColorAsState 带来的重组问题;
  • 组件 API 更简洁 :用单一的 Style 参数替代大量样式参数,接口更清晰;
  • 性能更优:样式在 Draw 和 Layout 阶段执行,跳过了 Composition 阶段,减少重组;
  • 标准化:提供一组统一的样式属性,让组件更容易接入样式体系。

Styles 并不是要取代 Modifier,而是更适合替代样式参数(如 paddingcolors)。对于交互、自定义绘制、属性堆叠、精准事件控制等行为,仍需使用 Modifier

Google 还贴心地提供了一个 agent skill 来帮助开发者在应用中使用新的 Styles API。

开始之前,我们先简单地看一下部分概念:

概念 说明
Style 定义 UI 元素外观的接口,类似 CSS 样式。可在本地自定义或通过主题统一配置。同一属性重复设置时,后者覆盖前者。
StyleScope Style 内部的接收者作用域,提供 background()padding()border() 等属性定义函数,以及对当前 StyleState 的访问。
StyleState 追踪元素的交互状态(如 isEnabledisPressedisChecked),支持自定义状态,用于实现条件样式。

当然,这些概念不用背,还是赶紧看代码!

开始

要使用这些 API,需要使用最新的 Compose foundation alpha 版本。

libs.versions.tomlapp/build.gradle.kts 中将 Compose 版本设置为官方文档当前示例中的 alpha 版本;实际使用时以最新 alpha 为准。

toml 复制代码
compose = "1.12.0-alpha03"
toml 复制代码
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "compose" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "compose" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "compose" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "compose" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "compose" }

属性一览

样式支持的属性覆盖面很广,但并非所有 Modifier 功能都能用样式替代。以下是完整的属性分类:

分组 属性 子组件可继承
布局与尺寸
内边距 contentPadding(内部)、externalPadding(外部),支持方向/水平/垂直/全方向
尺寸 fillWidth/fillHeight/fillSize()widthheightsize(支持 DpDpSizeFloat 分数)
定位 left/top/right/bottom 偏移
视觉外观
填充 backgroundforeground(支持 ColorBrush
边框 borderWidthborderColorborderBrush
形状 shapeclipborder 会使用此形状)
阴影 dropShadowinnerShadow
变换
空间移动 translationXtranslationYscaleX/scaleYrotationX/rotationY/rotationZ
控制 alphazIndex(堆叠顺序)、transformOrigin(枢轴点)
排版
样式 textStylefontSizefontWeightfontStylefontFamily
着色 contentColorcontentBrush(也用于图标样式)
段落 lineHeightletterSpacingtextAligntextDirectionlineBreakhyphens
装饰 textDecorationtextIndentbaselineShift

其中排版相关属性支持继承 ------父组件设置后会传播到所有子 Text 组件。

可以这么理解,和文字相关的,都支持继承。

这和 CSS 何其相似!

使用 Styles

使用 Styles 大概有三种方式!

1. 直接使用 Style 参数

暴露 Style 参数的组件可以直接在 lambda 中设置样式属性:

kotlin 复制代码
BaseButton(
    onClick = { },
    style = { }
) {
    BaseText("Click me")
}

在样式 lambda 内,可以设置各种属性,例如 contentPaddingbackground

kotlin 复制代码
BaseButton(
    onClick = {},
    style = { background(Color(0xFF1976D2)) }
) {
    Text("Blue", color = Color.White)
}

BaseButton(
    onClick = {},
    style = {
        background(Color(0xFF388E3C))
        contentPadding(horizontal = 24.dp, vertical = 12.dp)
    },
) {
    Text("Green (padded)", color = Color.White)
}

2. 通过 Modifier.styleable 应用样式

对于没有内置 style 参数的组件,可以使用 Modifier.styleable

kotlin 复制代码
Row(
    modifier = Modifier.styleable { }
) {
    BaseText("Content")
}

style 参数类似,你可以在 lambda 内包含 backgroundshape 等属性:

kotlin 复制代码
Row(
    modifier = Modifier
        .styleable {
            background(Color(0xFFE3F2FD))
            shape(RoundedCornerShape(12.dp))
            contentPadding(16.dp)
        }
        .fillMaxWidth(),
) {
    Text("styled via Modifier.styleable")
}

多个 Modifier.styleable 链式调用时,非继承属性会像多个 Modifier 一样作用在当前组件上;继承属性则由链中最后一个 styleable 决定。

使用 Modifier.styleable 时,你可能还需要创建并提供一个 StyleState,以应用基于状态的样式。

3. 定义独立样式复用

将样式抽为变量,可在多个组件间共享:

kotlin 复制代码
val style = Style { background(Color.Blue) }

// 通过 style 参数使用
BaseButton(onClick = { }, style = style) {
    BaseText("Button")
}

// 通过 Modifier.styleable 使用(需配合 StyleState)
val styleState = remember { MutableStyleState(null) }
Column(Modifier.styleable(styleState, style)) {
    BaseText("Column content")
}

也可以将同一个样式传递给多个组件:

kotlin 复制代码
val sharedStyle = Style {
    background(Color(0xFF6A1B9A))
    shape(RoundedCornerShape(8.dp))
    contentPadding(horizontal = 16.dp, vertical = 10.dp)
}

BaseButton(onClick = {}, style = sharedStyle) {
    Text("Shared on button", color = Color.White)
}

val columnState = remember { MutableStyleState(null) }
Column(
    modifier = Modifier
        .styleable(columnState, sharedStyle)
        .fillMaxWidth(),
) {
    Text("Same Style on a Column", color = Color.White)
}

属性覆盖与合并规则

样式属性不是累加的 ,而是以最后一次设置为准。这与 Modifier 的行为不同。可以在每行设置不同的属性来添加多个样式属性:

kotlin 复制代码
BaseButton(
    onClick = { },
    style = {
        background(Color.Blue)
        contentPaddingStart(16.dp)
    }
) {
    BaseText("Button")
}

同一属性重复设置时,后者覆盖前者:

kotlin 复制代码
val overrideStyle = Style {
    background(Color(0xFFD32F2F))
    background(Color(0xFF008080)) // final background
    contentPadding(64.dp)
    contentPaddingTop(16.dp)
}
BaseButton(onClick = {}, style = overrideStyle) {
    Text("Override", color = Color.White)
}

发现了吗?contentPaddingTop 会把顶部内边距改成 16.dp,其余方向仍沿用 contentPadding(64.dp) 的设置。

多个 Style 对象可以通过 then 合并,后者覆盖前者:

kotlin 复制代码
val style1 = Style { background(TealColor) }
val style2 = Style { contentPaddingTop(16.dp) }

BaseButton(
    style = style1 then style2,
    onClick = { },
) {
    BaseText("Click me!")
}

当多个样式指定相同属性时,最后设置的属性会被选中。因为样式中的属性不是累加的,最后传入的内边距会覆盖由初始 contentPadding 设置的水平内边距,最后一个背景色也会覆盖初始样式设置的背景色:

kotlin 复制代码
val s1 = Style {
    background(Color(0xFFD32F2F))
    contentPadding(32.dp)
}
val s2 = Style {
    contentPaddingHorizontal(8.dp)
    background(Color(0xFFBDBDBD))
}
BaseButton(onClick = {}, style = s1 then s2) {
    Text("关注 RockByte 公众号", color = Color.Black)
}

样式继承

排版和着色相关属性支持从父组件向下继承,覆盖优先级从高到低为:

优先级 方式 示例
1(最高) 组件直接参数 Text(color = Color.Red)
2 Style 参数 Text(style = Style { contentColor(Color.Red) })
3 Modifier.styleable Modifier.styleable { contentColor(Color.Red) }
4(最低) 父组件继承 父组件设置的排版/颜色属性

父组件设置的 contentColor 等属性会自动传播到所有子 Text 组件,子组件也可以通过自身样式覆盖继承值。下面是一个父组件设置文本属性的例子:

kotlin 复制代码
val styleState = remember { MutableStyleState(null) }
Column(
    modifier = Modifier.styleable(styleState) {
        background(Color.LightGray)
        val blue = Color(0xFF4285F4)
        val purple = Color(0xFFA250EA)
        val colors = listOf(blue, purple)
        contentBrush(Brush.linearGradient(colors))
    },
) {
    BaseText("Children inherit", style = { width(60.dp) })
    BaseText("certain properties")
    BaseText("from their parents")
}

子组件也可以通过自身样式覆盖父组件的继承值:

kotlin 复制代码
Column(
    modifier = Modifier.styleable {
        val blue = Color(0xFF4285F4)
        val purple = Color(0xFFA250EA)
        val colors = listOf(blue, purple)
        background(Brush.linearGradient(colors))
        contentPadding(32.dp)
    },
) {
    Box(
        modifier = Modifier.styleable(
            style = Style {
                background(Brush.linearGradient(listOf(Color.Red, Color.Blue)))
            },
        ),
    ) {
        BasicText("Children can override properties")
    }
    BasicText("set by their parents")
}

封装样式函数与 CompositionLocal

可以通过 StyleScope 的扩展函数封装常用样式组合:

kotlin 复制代码
fun StyleScope.outlinedBackground(color: Color) {
    border(2.dp, color)
    background(color.copy(alpha = 0.4f))
}

val customStyle = Style {
    outlinedBackground(Color.Red)
    contentPadding(horizontal = 20.dp, vertical = 12.dp)
    shape(RoundedCornerShape(8.dp))
}
BaseButton(onClick = {}, style = customStyle) {
    Text("outlinedBackground(Color.Red)", color = Color.White)
}

这里我不得不提一句,Styles 有一个我非常喜欢的地方:当我已经设置了 shape 之后,再使用 borderbackground 时,就不用分别给它们传递 shape。这一点在 Modifier 调用链里没这么直接。

样式也支持读取 CompositionLocal 中的设计令牌:

kotlin 复制代码
val buttonStyle = Style {
    contentPadding(12.dp)
    shape(RoundedCornerShape(50))
    background(Brush.verticalGradient(LocalCustomColors.currentValue.background))
}

交互状态处理

Styles API 内置了对常见交互状态的支持:PressedHoveredFocusedSelectedEnabledToggled

StyleState 是稳定的只读接口,用来追踪元素当前是否 enabledpressedfocused 等;在 StyleScope 中可以基于这些状态声明条件样式。

style 块中可以直接声明各状态下的样式:

kotlin 复制代码
val lightBlue = Color(0xFFBBDEFB)
val lightRed = Color(0xFFFFCDD2)

val interactiveStyle = Style {
    background(Color.White)
    border(1.dp, Color(0xFF9E9E9E))
    contentPadding(12.dp)
    focused {
        background(lightBlue)
    }

    pressed {
        background(lightRed)
        border(2.dp, Color(0xFFD32F2F))
    }
}
BaseButton(
    onClick = {},
    style = interactiveStyle,
) {
    Text(
        "Hover / Focus / Press me",
        style = TextStyle(color = Color.Black, fontSize = 16.sp),
    )
}

状态还可以嵌套组合,例如同时处理悬停+按下的场景:

kotlin 复制代码
hovered {
    background(lightPurple)
    pressed {
        background(lightOrange)  // 悬停时按下
    }
}
pressed {
    background(lightRed)         // 非悬停时按下
}

自定义组件的状态支持

创建自定义 styleable 组件时,需要将 interactionSource 连接到 styleState,并把同一个 interactionSource 传给相关的交互 Modifier,例如 clickablefocusable

kotlin 复制代码
@Composable
private fun GradientButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    style: Style = Style,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable RowScope.() -> Unit,
) {
    val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
    val styleState = rememberUpdatedStyleState(interactionSource) {
        it.isEnabled = enabled
    }
    Row(
        modifier = modifier
            .clickable(
                onClick = onClick,
                enabled = enabled,
                interactionSource = interactionSource,
                indication = null,
            )
            .styleable(styleState, baseGradientButtonStyle then style),
        content = content,
    )
}

基于此基础组件,可以方便地创建具有交互效果的派生组件:

kotlin 复制代码
@Composable
fun LoginButton() {
    val loginButtonStyle = Style {
        pressed {
            background(Brush.linearGradient(listOf(Color.Magenta, Color.Red)))
        }
    }
    GradientButton(onClick = { }, style = loginButtonStyle) {
        BaseText("Login")
    }
}

样式动画

状态切换时的样式变化支持内置动画。只需在状态块内用 animate 包裹属性即可。

不仅如此,animate 还支持自定义 animationSpec,例如补间动画和弹簧动画:

kotlin 复制代码
val animatingStyle = Style {
    externalPadding(48.dp)
    border(3.dp, Color.Black)
    background(Color.White)
    size(100.dp)
    transformOrigin(TransformOrigin.Center)
    pressed {
        animate(tween(durationMillis = 400)) { // 补间动画
            borderColor(Color.Magenta)
            background(Color(0xFFB39DDB))
        }
        animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) { // 弹簧动画
            scale(1.2f)
        }
    }
}

val interactionSource = remember { MutableInteractionSource() }
val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
Box(
    modifier = Modifier
        .fillMaxWidth()
        .height(200.dp),
    contentAlignment = Alignment.Center,
) {
    Box(
        modifier = Modifier
            .clickable(
                interactionSource = interactionSource,
                indication = null,
                onClick = {},
            )
            .styleable(styleState, animatingStyle),
    )
}

自定义状态样式

除了内置的交互状态,你还可以定义自定义状态。以媒体播放器为例,可以根据播放状态(Stopped / Playing / Paused)应用不同样式。

1. 定义状态枚举和键

kotlin 复制代码
enum class PlayerState {
    Stopped,
    Playing,
    Paused
}

val playerStateKey = StyleStateKey(PlayerState.Stopped)

2. 创建扩展函数

kotlin 复制代码
var MutableStyleState.playerState
    get() = this[playerStateKey]
    set(value) { this[playerStateKey] = value }

fun StyleScope.playerPlaying(block: () -> Unit) {
    state(playerStateKey, block) { _, state -> state[playerStateKey] == PlayerState.Playing }
}

fun StyleScope.playerPaused(block: () -> Unit) {
    state(playerStateKey, block) { _, state -> state[playerStateKey] == PlayerState.Paused }
}

3. 在组件中使用

在组合项中定义 styleState 并将 styleState.playerState 设置为传入的状态。将 styleState 传递到修饰符的 styleable 函数中:

kotlin 复制代码
@OptIn(ExperimentalFoundationStyleApi::class)
@Composable
private fun MediaPlayer(
    url: String,
    modifier: Modifier = Modifier,
    style: Style = Style,
    state: PlayerState = remember { PlayerState.Paused },
) {
    val styleState = remember { MutableStyleState(null) }
    styleState.playerState = state
    Box(
        modifier = modifier
            .fillMaxWidth()
            .height(80.dp)
            .styleable(styleState, style),
        contentAlignment = Alignment.Center,
    ) {
        Text(
            text = "MediaPlayer(${state.name}): $url",
            style = TextStyle(fontSize = 16.sp),
        )
    }
}

这里我只是做了一个假的 MediaPlayer 用于测试。

4. 定义状态样式

stylelambda 内,使用之前定义的扩展函数为自定义状态应用基于状态的样式:

kotlin 复制代码
@Composable
fun StyleStateKeySample() {
    val style = Style {
        borderColor(Color.Gray)
        playerPlaying {
            animate { borderColor(Color.Green) }
        }
        playerPaused {
            animate { borderColor(Color.Blue) }
        }
    }
    // 修改 state 参数即可改变样式,也可以连接 ViewModel 来动态切换状态
    MediaPlayer(
        url = "https://example.com/media/video",
        style = style,
        state = PlayerState.Stopped,
    )
}

一点想法

Styles API 目前仍是实验性 API,Material 对 Styles 的支持也还在后续版本中。落地时可以先从自定义设计系统或少量自定义组件开始,不建议直接把现有 Modifier 用法全部迁移过去。

目前看来,它比较适合解决三类问题:组件样式参数过多、交互状态样式分散、设计系统需要复用一组样式规则。与此同时,Modifier 仍然负责交互、自定义绘制和部分无法由 Style 表达的行为。

我自己比较强烈的感受是,Styles API 在强化"状态表示 UI"这个概念。过去很多样式都写在 Modifier 调用链里,调用顺序会影响最终结果,读起来多少有一点命令式编程的味道,也不像真正的 DSL。

新的 Style 写法把样式、状态和状态下的样式变化放在同一个声明式结构里,DSL 的感觉更强,也更适合描述 UI 在不同状态下应该呈现什么样子。虽然它现在还很早期,但我预感这种围绕状态组织样式的方式,可能会是 Compose 后续演进的一个趋势。

未完待续

相关推荐
墨狂之逸才19 小时前
Android TV WebView 遥控器按键处理:从全透传到白名单
android
plainGeekDev1 天前
MVC 写法 → MVVM
android·java·kotlin
恋猫de小郭1 天前
Flutter Patchwork,不用 Fork 改依赖包源码的第三方工具
android·前端·flutter
三少爷的鞋1 天前
“结构化”这个词,本质上就是——把混乱的东西变成有组织、有规则、有边界的东西
android
方白羽2 天前
Android Gradle 缓存与文件目录深度解析
android·gradle·android studio
曲幽2 天前
Termux里的二进制和脚本,到底怎么运行才不踩坑?Termux-service 保活妙招!
android·termux·nohup·services·wake-lock
plainGeekDev2 天前
单例模式 → object 声明
android·java·kotlin
程序员陆业聪2 天前
读者点单·03|Compose 与传统 View 混用的 12 个真实坑
android
程序员陆业聪2 天前
读者点单·02|Android 启动优化实战:Trace 抓取→Application 编排→冷启动全流程拆解
android