第4章 Button组件与点击事件:FilledButton、TextButton、OutlinedButton,点击响应与禁用状态

Button 组件是 Compose 中触发用户操作的核心入口。Material3 提供了三种视觉风格的按钮:FilledButton(实心按钮)、TextButton(文字按钮)和 OutlinedButton(描边按钮)。本章将深入讲解它们的 API 差异、点击事件绑定、禁用状态控制以及常见陷阱。

Button组件家族:视觉风格与事件传递 三种 Button 类型与点击事件流程 💡 点击事件统一通过 onClick 参数绑定,enabled 参数控制是否响应。 所有按钮内部已处理触摸反馈和语义化,开发者只需关注业务逻辑。 FilledButton 最常用、高强调 默认颜色 primary TextButton 低强调、仅文字 无边框/无背景 OutlinedButton 中等强调、描边 有边框无背景 用户点击 → onClick 回调 enabled 为 false 时不触发 enabled = true 正常执行点击逻辑 enabled = false 点击无响应,视觉变灰

4.1 三种按钮的定位与区别

在 Material Design 3 中,按钮按视觉优先级分为三层。下表总结了核心差异和使用场景:

按钮类型 强调级别 默认外观 适用场景 点击区域反馈
FilledButton 高 实心背景 + 圆角 主要操作、提交、确认 涟漪 (Ripple) + 遮罩
TextButton 低 仅文字,无边框/背景 次要操作、导航、取消 涟漪 (收缩背景)
OutlinedButton 中 文字 + 描边边框 中等操作、对比选项 涟漪 (拉伸背景)
选型建议:一个页面中应只有一个 FilledButton 作为主要行动点(CTA),TextButton 用于次要或取消操作,OutlinedButton 用于需要视觉区分的中间选项。

4.2 声明式点击事件与状态管理

Compose 中的点击事件不再是传统的 setOnClickListener,而是以 lambda 参数形式注入:

FilledButton(
    onClick = { /* 点击后执行的代码 */ }
) {
    Text("提交")
}

关键特性:

  • 闭包捕获:lambda 可以自动捕获外部的 Compose 状态(如 mutableStateOf 变量),状态变化会触发重组。
  • 不要阻塞主线程:onClick 内不要运行耗时任务;如需异步,使用 LaunchedEffect 或 rememberCoroutineScope。
  • 事件防抖:如果按钮频繁点击,可在 onClick 内部加入 remember 锁或使用 throttleLatest。

4.3 FilledButton 基础用法

FilledButton 是 Material3 的 Button 组件(注意命名与旧版不同),完整签名:

@Composable
fun FilledButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    shape: Shape = ButtonDefaults.filledShape,
    colors: ButtonColors = ButtonDefaults.filledButtonColors(),
    elevation: ButtonElevation? = ButtonDefaults.filledButtonElevation(),
    border: BorderStroke? = null,
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable RowScope.() -> Unit
)

实战示例:计数器

@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("当前计数: $count", fontSize = 28.sp)

        Spacer(modifier = Modifier.height(24.dp))

        FilledButton(
            onClick = { count++ },
            enabled = count < 10
        ) {
            Text("增加 (上限10)")
        }

        Spacer(modifier = Modifier.height(12.dp))

        FilledButton(
            onClick = { count = 0 },
            colors = ButtonDefaults.filledButtonColors(
                containerColor = MaterialTheme.colorScheme.secondary
            )
        ) {
            Text("重置")
        }
    }
}
📘 注意:Material3 中 FilledButton 等同于旧版 Button,但在新版本中为了避免重名改为显式调用。如果使用 import androidx.compose.material3.Button 依然有效,但推荐使用 FilledButton 以增强可读性。

4.4 TextButton 与 OutlinedButton 用法

它们的 API 签名与 FilledButton 几乎一致,仅默认颜色和形状不同。简单示例:

@Composable
fun ButtonVariantsDemo() {
    var clickedText by remember { mutableStateOf("") }

    Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
        TextButton(onClick = { clickedText = "文字按钮" }) {
            Text("TextButton")
        }

        OutlinedButton(onClick = { clickedText = "描边按钮" }) {
            Text("OutlinedButton")
        }

        if (clickedText.isNotEmpty()) {
            Text("上次点击: $clickedText")
        }
    }
}

自定义外观:通过 colors 参数修改各状态颜色:

OutlinedButton(
    onClick = { /* ... */ },
    colors = ButtonDefaults.outlinedButtonColors(
        contentColor = Color.Red,
        disabledContentColor = Color.Gray
    )
) {
    Text("自定义颜色")
}

4.5 禁用状态控制

所有按钮都通过 enabled 参数控制可否点击。当 enabled = false 时:

  • onClick 回调不会触发
  • 视觉上变为半透明或灰色(由主题决定)
  • 涟漪反馈消失

示例:加载状态中禁用按钮

@Composable
fun LoginButton(isLoading: Boolean) {
    var loading by remember { mutableStateOf(isLoading) }

    FilledButton(
        onClick = {
            loading = true
            // 模拟网络请求
            // 实际应使用 LaunchedEffect
        },
        enabled = !loading
    ) {
        if (loading) {
            CircularProgressIndicator(
                modifier = Modifier.size(20.dp),
                strokeWidth = 2.dp,
                color = MaterialTheme.colorScheme.onPrimary
            )
        } else {
            Text("登录")
        }
    }
}
⚠️ 常见错误:不要直接在 onClick 内部调用 Thread.sleep() 或网络请求,这会导致 ANR。正确的做法是使用 LaunchedEffect 或 rememberCoroutineScope 启动协程。

4.6 事件传递与防抖优化

在 Compose 中,按钮点击事件不会像 View 系统那样冒泡。每个组件独立处理其点击。如果需要全局拦截或统计,可在上层用 Modifier.clickable。

防抖(Throttle)实现:避免用户快速重复提交:

@Composable
fun DebouncedButton(onSubmit: () -> Unit) {
    var lastClickTime by remember { mutableStateOf(0L) }

    FilledButton(
        onClick = {
            val now = System.currentTimeMillis()
            if (now - lastClickTime > 500L) {
                lastClickTime = now
                onSubmit()
            }
        }
    ) {
        Text("提交")
    }
}

4.7 常见错误与调试

错误现象 原因 解决方案
按钮点击无响应 enabled = false 或 onClick 为 null 检查 enabled 和 onClick 是否传入有效 lambda
按钮文字不居中 contentPadding 过大或 content 内使用了非 RowScope 布局 确保 content 内只有 Text 或图标,不要嵌套 Column
视觉上看不到按钮 颜色与背景一致或透明 检查 colors 参数,调试时可给 Button 加背景色测试
多次点击触发多次 缺少防抖或协程控制 加入状态锁或使用 throttleLatest
调试技巧:在按钮外层添加 Modifier.clickable 并打印日志,可以确认点击事件是否被上层拦截。使用 Layout Inspector 查看组件树层级。