Compose 修饰符 - 样式 Style API(概念篇)

一、简介

Modifier承载的设置太多,臃肿的大量链式调用影响阅读,不同类型的调用顺序还关系到重组性能,把视觉效果类的设置给抽出来成了 Style API。

  • 简化了基于状态的样式设置:提供了一种更简洁、更声明式的方式来定义基于不同状态(例如悬停、聚焦、按压)而变化的样式,与 Modifier 系统相比,可显著减少样板代码。
  • 改进了动画状态转换:允许在状态之间对样式属性进行内置动画处理,具有理想的性能特征,避免了当前 animateColorAsState() 方法中发生的重组。
  • 简化了组件 API:通过引入单个 Style 参数进行自定义,组件 API 得到了大幅简化,并提供了更高的灵活性。
  • 减少重组次数,从而提升修饰符的性能:样式在 Compose 的绘制和布局阶段运行,跳过组合阶段。
  • 更标准化的 API 集:一组标准的样式属性可让任何组件都可设置Style。
Style一种界面,用于定义界面元素的外观,具有一组可设置样式的标准属性。它类似于 CSS 样式,可以在本地或通过主题进行自定义。样式会相互覆盖;如果将某个属性设置两次(例如background()),则只会保留一个最终值。
StyleScope

Style 中 applyStyle() 函数的接收器范围。它提供了一些函数来定义视觉属性(contentPadding、background、border等)并访问当前的 StyleState。

StyleState提供可在样式中使用的状态(例如 isEnabled、isPressed、isChecked、自定义状态),以根据不同的条件应用样式。

1.1 对比

ModifierStyle
主要目标定义行为、语义和复杂布局。修饰符可动态操控特定可组合函数中的各个元素,不会从主题向下传递。定义视觉外观、各个商品的大小和可设置主题的属性。样式在主题级别运行,可在组件级别覆盖。它们会向下传递,并跨不同的可组合项应用样式。
逻辑相加 - 修饰符组合在一起形成新的结果。可覆盖 - 样式中设置的最后一个属性生效。样式充当单个属性层,这些属性会根据定义的优先级层次结构相互替换。
主题难以提升为主题,通常单独使用。根据设计,样式可设置主题(可访问 CompositionLocal),并且只需定义一次即可在多个组件中使用。
性能更新通常需要 Compose 的所有三个阶段:组合、布局和绘制。如需使修饰符实现良好的动画效果,通常需要编写基于 lambda 的版本。跳过组合阶段,仅在布局和绘制阶段处于活动状态,从而减少重组。需要更少的对象分配。
动画需要使用单独的动画原语,例如 animate*AsState。内置 animate { } API,可为您处理一些动画。

1.1.1 Modifier 的限制

在当前的 Compose 环境中,Modifier 具有许多优势,不过 Style 解决了 Modifier 的一些限制。

  • 修饰符通常在组合阶段创建。更新可能会强制完全重新运行 Composition、Layout 和 Draw,即使是颜色等细微的视觉变化也是如此,除非您创建基于 lambda 的修饰符。
  • 条件修饰符需要在流畅链中实现破坏性的 if-else 逻辑。 为它们添加动画效果需要手动设置状态样板,并且缺少高性能的“自动动画”机制。
  • 修饰符会叠加,而不是替换。您无法替换组件的默认边框,只能在上面绘制第二个边框。
  • 修饰符很难抽象为全局主题。因此,主题通常存储原始值,而不是可重复使用的修饰符配置。

1.1.2 Style 的限制

虽然 Style 可以弥补 Modifier 的一些不足,但它们也存在一些限制,这表明它们无法完全取代 Modifier。

  • Style 是专门的 Modifier。虽然 Modifier 可以执行 Style 能执行的任何操作,但反之却不成立。因此 Style 可以补充 Modifier 但不能取代 Modifier。
  • Style 仅限于视觉配置(背景、内边距、边框), 它们无法处理点击逻辑、手势检测或无障碍语义等行为。
  • 将 Style 解析为最终状态比应用单个 Modifier 更耗费资源。系统必须生成一个包含所有可能属性值的数据结构,而继承属性的查找会进一步增加复杂性。

1.2 何时使用

虽然是否使用 Style 很大程度上取决于您的应用和使用场景,但以下指南有助于确定何时优先选择 Style 而不是 Modifier。

  • 为了实现主题范围内的一致性:Style 旨在“提升”到全局主题中。您可以在主题中定义单个Style,以便在整个应用中创建统一的外观,而无需向每个组件传递重复的 Modifier。
  • 在执行频繁动画时:Style 会在布局和绘制阶段进行评估,从而允许颜色或缩放等属性在完全绕过组合阶段的情况下实现动画效果。这可显著减少性能开销。在执行视觉属性动画时,请使用 Style 而不是Modifier。
  • 替换与堆叠:如果您需要替换默认属性,请使用Style。Modifier 是累加的(添加一个边框会堆叠第二个边框),而 Style 使用“后写胜出”逻辑,因此可以更轻松地替换背景或内边距,而不会造成视觉混乱。
  • 自定义 Material 组件:如果某个 Material 组件提供 Style 参数,建议使用此方法进行自定义。借助这些Style,您可以访问和修改可组合函数内部结构中可能无法访问的特定属性。

1.3 性能优势

Style 在 Compose 的布局和绘制阶段运行。这样一来,就不需要创建基于 Lambda 的修饰符,因为样式始终会跳过组合阶段。

  • 阶段转换:Style 通常以绘制阶段为目标。当值发生更改时 Compose 只会使受影响的阶段失效(例如绘制),而不会触发完整的重组或重新布局。
  • 延迟分配:Style 会延迟动画资源分配,直到动画实际开始。这样可以减少初始撰写期间所需的工作量。
  • 减少了对象开销:链式调用 Modifier 会为每个属性(例如padding、border)分配一个对象。Style 使用单个 Lambda 来应用多个属性,从而显著减少内存分配。如果某个 Style 是在主题中定义的,则该 Lambda 会在所有使用相应主题的组件之间共享。

下表显示了 Compose 1.11.0-alpha06 的 Style 内部性能基准测试的示例结果,并与不使用 Style 的 Compose 实现进行了比较。basic_box_border_change 测试突显了 Style 系统在避免属性更新期间分配多个 Modifier 对象方面的优势,从而使分配量大幅减少了约 77%,时间减少了约 59%。

测试方法说明时间变更分配变更
basic_box_border_change切换 Box 的边框颜色,以衡量更新性能。-59.91%-77.22%
input_state_basic_box比较基于样式的悬停/焦点/按下状态与手动互动状态收集。-5.24%-14.72%
basic_box测量具有 5 个链式修饰符的 Box 的初始组合和布局。-4.78%-6.60%
basic_text使用硬编码的字符串渲染五个 BasicText 组件。+0.62%+2.41%
basic_text_provided_color比较了通过样式设置文字颜色与使用 CompositionLocalProvider 设置文字颜色。+5.86%+9.82%

1.4 样式覆盖

styleable{ } 代码块中的属性重复定义会被覆盖,会采用代码块中的最后一次指定值。若是多次链式调用 Modifier.styleable() ,非继承属性是累加性的,其行为类似于 Modifier 多次定义相同属性的情况。对于继承属性,这些属性会被覆盖,链中最后一个 styleable 修饰符将设置其值。

1.5 样式继承

排版相关的属性会传播到子组件,为子组件设置的样式会覆盖掉继承的父样式。

优先级方法效果
最低父级样式对于可继承的属性(排版/颜色),从父级传递下来。
修饰符链Modifier.styleable{ contentColor(Color.Red)
样式参数本地样式覆盖 Text(style = Style { contentColor(Color.Red)}
最高可组合项上的直接实参替换所有内容;例如,Text(color = Color.Red)

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(
        text = "Children can ",
        style = {
            contentBrush(Brush.linearGradient(listOf(Color.Red, Color.Blue)))
        }
    )
    // 这里会进行继承
    BaseText("certain properties")
    BaseText("from their parents")
}

二、简单使用

.styleable()

fun Modifier.styleable(styleState: StyleState? = null, style: Style): Modifier

fun Modifier.styleable(styleState: StyleState?, vararg styles: Style): Modifier

2.1 通过参数传递给已支持的组件

BaseButton(
    onClick = { },
    style = { background(Color.Blue) }
) {
    BaseText("Click me")
}

2.2 通过 Modifier.styleable() 应用样式

Box(
    modifier = Modifier.styleable { background(Color.Blue) }
) {...}

2.3 提取 Style 重复使用

val style = Style { background(Color.Blue) }

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

// 通过 Modifier 应用
Box(
    modifier = Modifier.styleable(null, style)
) {...}

2.4 自定义 Style 属性

对 StyleScope 使用扩展属性来创建自定义效果。

暂时只支持整合 Style 已有的属性 20260511。

// 定义
fun StyleScope.outlinedBackground(color: Color) {
    border(1.dp, color)
    background(color)
}
// 使用
val customExtensionStyle = Style {
    outlinedBackground(Color.Blue)
}

2.5 覆盖方式

@Composable
fun Demo(
    modifier: Modifier = Modifier,
    style: Style = Style
) {
    // 外部覆盖内部
    Box(modifier = modifier.styleable(styleState, {...} then style))
    // 内部覆盖外部
    Box(modifier = modifier.styleable(styleState, style then {...}))
}

三、最佳实践

3.1 正确做法

3.1.1 使用Style实现视觉效果,使用Modifier实现行为

使用 Styles API 进行视觉配置(背景、内边距、边框),并为点击逻辑、手势检测或无障碍功能等行为保留修饰符。

3.1.2 自定义组件公开 Style 参数供外部使用

@Composable
fun Demo(
    modifier: Modifier = Modifier,
    style: Style = Style
) {...}

3.1.3 将组件的视觉效果参数替换为 Style

// 之前
@Composable
fun OldButton(
    background: Color,
    fontColor: Color
) {}

// 现在
@Composable
fun NewButton(style: Style = Style) {}

3.1.4 优先使用 Style 实现动画

使用内置 animate 块进行基于状态的 Style 设置,并使用动画来提高性能,而不是使用Modifier。

3.1.5 利用“后写优先”原则

利用 style 属性会覆盖而非堆叠这一事实。,使用此属性可替换默认组件边框或背景,而无需使用多个参数。

3.1.6 为子系统值更改创建一个 Style

如果要在深色模式和浅色模式之间切换,请查询现有主题值(通过 CompositionLocal)以动态更改 Style。

// 使用 CompositionLocals 或主题化的值来创建单一的样式。
val buttonStyle = Style {
    background(colors.brandSecondary)
    shape(shapes.small)
}

3.1.7 当组件在主题定义之间存在根本差异时,替换整个Style

如果 Style 对象在主题级别上存在根本差异,可以替换整个 Style 对象。例如,如果要创建一个应用,该应用针对每个产品/页面或产品/服务提供不同的主题,并且 Style 的许多属性都不同,那么在主题级别上替换整套 Style 是可以接受的。

// 推荐做法:当多个属性不同时,整体切换不同的样式 - 例如产品 A 和产品 B 是两个提供不同主题的白标应用。
val productBThemedButton = Style {
    shape(shapes.small)
    background(colors.brandSecondary)
    // 其他属性本质上是不同的。
}

val productAThemedButton = Style {
    shape(shapes.large)
    background(colors.brand)
    // 其他属性本质上是不同的。
}

3.2 错误做法

3.2.1 使用 Style 实现互动逻辑

请勿尝试在 Style 中处理 onClick() 或手势检测。Style 仅限于基于状态的视觉配置,因此不应处理业务逻辑;相反,它们只应根据状态具有不同的视觉效果。

3.2.2 将自定义 Style 用作默认参数提供

应始终使用 style: Style = Style 进行声明。如需添加自定义的默认效果,将传入的 Style 与自定义的 Style 合并。

@Composable
fun BadButton(
    modifier: Modifier = Modifier,
    // 不要将自定义 Style 用作默认参数
    style: Style = Style { background(Color.Red) }
) {}
@Composable
fun GoodButton(
    modifier: Modifier = Modifier,
    style: Style = Style
) {
    val defaultStyle = Style { background(Color.Red) }
    Box(
        // 应该在内部将默认值和传入的进行合并
        modifier = modifier.styleable(styleState, defaultStyle, style)
    ) {}
}

3.2.3 向基于布局的可组合项提供 Style 参数

虽然可以向任何组件提供Style,但基于布局的组件或屏幕级组件不应接受Style,因为从消费者的角度来看,不清楚 Style 在此级别上的作用。 Style 是为控件设计的,不一定是为布局设计的。

3.2.4 在组件内创建 Style

对 CompositionLocals 不要在定义 Style 时读取,而是在使用 Style 时读取。否则当实际使用 Style 时 CompositionLocal 的状态可能已更改,从而导致 Style 未被更新。

// 不要像这样在组件内部通过局部变量来创建 Style
// 当值变化后,Style不会得到更新
@Composable
fun containerStyle(): Style {
    val background = MaterialTheme.colorScheme.background
    val onBackground = MaterialTheme.colorScheme.onBackground
    return Style {
        background(background)
        contentColor(onBackground)
    }
}
val StyleScope.colors: JetsnackColors
    get() = JetsnackTheme.LocalJetsnackTheme.currentValue.colors
val StyleScope.typography: androidx.compose.material3.Typography
    get() = JetsnackTheme.LocalJetsnackTheme.currentValue.typography
val StyleScope.shapes: Shapes
    get() = JetsnackTheme.LocalJetsnackTheme.currentValue.shapes

val button = Style {
    background(colors.brandSecondary)
    shape(shapes.small)
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值