Jetpack系列之Compose Button
本文介绍了Jetpack Compose中Button组件的使用与自定义封装方法。主要内容包括:1) Button基础属性说明,如onClick、enabled、modifier等;2) VectorDrawable的优势与应用;3) 自定义按钮封装方案,包括AlphaIconButton(支持透明度变化)、AppButton(支持多种状态颜色变化)等组件实现;4) 颜色扩展工具类,提供颜色加深、
·
(一)概述
在 Jetpack Compose 中,Button 是一个常用的 UI 组件,主要用于执行交互操作,如点击事件。它允许用户触发特定的行为或事件,例如提交表单、导航到其他页面、执行某个功能等。
(二)Button 属性说明
- onClick:按钮点击时触发的回调。
- enabled:控制按钮是否可用,默认为 true。当设置为 false 时,按钮会变成禁用状态,不能响应点击事件,并且通常会在 UI 上表现为灰色。
- modifier:用于修饰按钮的外观,比如修改大小、边距、对齐方式等。Modifier 是 Compose 中所有 UI 元素的通用修饰器。注意:fillMaxWidth() 等价于layout_width="match_parent",fillMaxHeight() 等价于 layout_height="match_parent",fillMaxSize() 等价于 width = match_parent, height = match_parent。Row: 水平排列,Column: 垂直排列。
- contentColor:控制按钮内文本或图标的颜色。默认值:根据当前主题的 Button 主题自动选择颜色。
- containerColor:设置按钮的背景颜色。你可以为不同的按钮状态(如正常、按下、禁用等)提供不同的颜色。默认值:根据当前主题的 Button 主题自动选择颜色。
- elevation:设置按钮的阴影高度。阴影通常用于表现按钮的立体感。
- shape: 用于定义按钮的形状,例如圆角、矩形等。默认值:RoundedCornerShape(4.dp)(一般来说按钮是有圆角的)
- interactionSource: 用于管理按钮的交互状态,比如是否正在被点击、是否被按压等。它可以用来实现按钮的动态效果,比如点击时改变颜色。默认值:remember { MutableInteractionSource() }
- border: 用来设置按钮的边框,可以自定义边框的宽度、颜色等。
- focusable: 控制按钮是否可以获得焦点。按钮默认是可聚焦的。默认值:true
- textStyle: 用于设置按钮文本的样式,例如字体大小、字体、加粗等。默认值:MaterialTheme.typography.button
需要说明的是,Button pressed 状态默认有 ripple,Button 无法关闭 ripple,无法自定义 pressed 颜色动画,没有“颜色反馈”,所以一般需要自定义。
(三)VectorDrawable 介绍
VectorDrawable 是一种用“路径数据”描述图形的 Drawable,用来替代多套位图资源(png / webp),实现分辨率无关(减少资源)、可主题化(动态着色)、可动画的 UI 图标体系。对比PNG等格式,它的优势是自动适配dpi,缩放不失真,体积小,使用tint着色,android studio自带生成工具,完美适配Compose。需要注意的是,Compose 的 Icon(tint = …) 是“运行时 UI 主题着色”,VectorDrawable 的 android:tint 是“资源层的默认染色”。
(四)Button 封装
1. 支持Alpha的IconButton
package com.leo.wechat.ui.component.button
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.leo.wechat.ext.throttledClick
// Default tokens
object AppIconAlpha {
const val NORMAL = 0.9f
const val PRESSED = 0.6f
const val DISABLED = 0.38f
}
@Composable
private fun resolveIconColor(
enabled: Boolean,
isPressed: Boolean,
contentColor: Color,
normalAlpha: Float,
pressedAlpha: Float,
disabledAlpha: Float,
): Color = when {
!enabled -> contentColor.copy(alpha = disabledAlpha)
isPressed -> contentColor.copy(alpha = pressedAlpha)
else -> contentColor.copy(alpha = normalAlpha)
}
@Composable
fun AlphaIconButton(
painter: Painter,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
enableThrottle: Boolean = true,
contentColor: Color = LocalContentColor.current,
iconSize: Dp = 20.dp,
iconMinClickAreaSize: Dp = 48.dp,
normalAlpha: Float = AppIconAlpha.NORMAL,
pressedAlpha: Float = AppIconAlpha.PRESSED,
disabledAlpha: Float = AppIconAlpha.DISABLED,
contentDescription: String? = null,
) {
// InteractionSource: Compose 的交互事件总线
val interactionSource = remember { MutableInteractionSource() }
// collectIsPressedAsState: 把按下事件转成可响应的状态
val isPressed by interactionSource.collectIsPressedAsState()
val tint by animateColorAsState(
targetValue = resolveIconColor(
enabled = enabled,
isPressed = isPressed,
contentColor = contentColor,
normalAlpha = normalAlpha,
pressedAlpha = pressedAlpha,
disabledAlpha = disabledAlpha
),
// durationMillis = 120, pressed / release 属于“即时反馈”
animationSpec = tween(120),
label = "AlphaIconButtonTint"
)
Box(
modifier = modifier
.size(iconMinClickAreaSize)
.throttledClick(
interactionSource = interactionSource,
indication = null,// remove ripple
enabled = enabled,
enableThrottle = enableThrottle,
role = Role.Button,
onClick = onClick
),
contentAlignment = Alignment.Center
) {
Icon(
painter = painter,
contentDescription = contentDescription,
modifier = Modifier.size(iconSize),
tint = tint
)
}
}
AlphaIconButton.kt 说明:自定义一个支持Alpha的IconButton
2. 支持带图标的Button
package com.leo.wechat.ui.component.button
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
data class AppButtonColors(
val containerColor: Color,
val contentColor: Color,
val disabledContainerColor: Color,
val disabledContentColor: Color,
val pressedContainerColor: Color,
val pressedContentColor: Color,
)
@Composable
fun AppButtonColors.resolveContainerColor(
enabled: Boolean,
pressed: Boolean,
): Color =
when {
!enabled -> disabledContainerColor
pressed -> pressedContainerColor
else -> containerColor
}
@Composable
fun AppButtonColors.resolveContentColor(
enabled: Boolean,
pressed: Boolean,
): Color =
when {
!enabled -> disabledContentColor
pressed -> pressedContentColor
else -> contentColor
}
AppButtonColors.kt 说明:负责Button颜色模型及颜色解析
package com.leo.wechat.ui.component.button
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import com.leo.wechat.ext.pressed
object AppButtonColorDefaults {
@Composable
fun primary(): AppButtonColors {
val container = MaterialTheme.colorScheme.primary
val content = MaterialTheme.colorScheme.onPrimary
return AppButtonColors(
containerColor = container,
contentColor = content,
pressedContainerColor = container.pressed(),
pressedContentColor = content.copy(alpha = 0.9f),
disabledContainerColor = container.copy(alpha = 0.5f),
disabledContentColor = content.copy(alpha = 0.6f),
)
}
// other colors methods
}
AppButtonColorDefaults.kt 说明:封装一些用常用按钮配色(支持禁用,按下)
package com.leo.wechat.ext
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
/**
* Darken a color by overlaying black.
*
* @param amount 0f..1f, recommended 0.06f ~ 0.12f
*/
fun Color.darken(amount: Float): Color {
return lerp(this, Color.Black, amount.coerceIn(0f, 1f))
}
/**
* Lighten a color by overlaying white.
*
* @param amount 0f..1f, recommended 0.06f ~ 0.12f
*/
fun Color.lighten(amount: Float): Color {
return lerp(this, Color.White, amount.coerceIn(0f, 1f))
}
fun Color.pressed(): Color = darken(0.08f)
AppColorExt.kt 说明:拓展一些Color方法,如加深,变浅等
package com.leo.wechat.ext
import androidx.compose.foundation.Indication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.semantics.Role
/**
* Click modifier with optional throttle support.
*
* @param enableThrottle Whether to enable click throttling.
* @param intervalMillis Throttle interval in milliseconds, effective only when
* enableThrottle is true.
*/
fun Modifier.throttledClick(
interactionSource: MutableInteractionSource?,
indication: Indication?,
enabled: Boolean = true,
enableThrottle: Boolean = true,
intervalMillis: Long = 800L,
role: Role? = null,
onClick: () -> Unit,
): Modifier = composed {
val shouldThrottle = enableThrottle && intervalMillis > 0L
if (!shouldThrottle) {
return@composed clickable(
interactionSource = interactionSource,
indication = indication,
enabled = enabled,
role = role,
onClick = onClick
)
}
// Cache the time of the last "valid click"
var lastClickTime by remember { mutableLongStateOf(0L) }
clickable(
interactionSource = interactionSource,
indication = indication,
enabled = enabled,
role = role
) {
val now = System.currentTimeMillis()
if (now - lastClickTime >= intervalMillis) {
lastClickTime = now
onClick()
}
}
}
ModifierExt.kt 说明:为button等组件添加防暴力点击等。
package com.leo.wechat.ui.component.button
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import com.leo.wechat.ext.throttledClick
enum class AppButtonIconPosition {
Start,
Top
}
@Composable
fun AppButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
enableThrottle: Boolean = true,
colors: AppButtonColors = AppButtonColorDefaults.primary(),
style: TextStyle = MaterialTheme.typography.labelLarge,
shape: Shape = RoundedCornerShape(12.dp),
icon: Painter? = null,
iconPosition: AppButtonIconPosition = AppButtonIconPosition.Start,
iconSpacerModifier: Modifier = Modifier.width(8.dp),
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed = remember { mutableStateOf(false) }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> isPressed.value = true
is PressInteraction.Release,
is PressInteraction.Cancel,
-> isPressed.value = false
}
}
}
val containerColor by animateColorAsState(
targetValue = colors.resolveContainerColor(enabled, isPressed.value),
animationSpec = tween(120),
label = "AppButtonContainerColor"
)
val contentColor by animateColorAsState(
targetValue = colors.resolveContentColor(enabled, isPressed.value),
animationSpec = tween(120),
label = "AppButtonContentColor"
)
// Surface 是 Compose 中最实用的容器组件:自动处理颜色对比度,简化了阴影和边框的实现。
// 实际上 Scaffold 也是使用 Surface 实现的。如果需要完全自定义可以用 Box 。
Surface(
modifier = modifier
.heightIn(min = 36.dp)
.clip(shape)
.throttledClick(
interactionSource = interactionSource,
indication = null,// remove ripple
enabled = enabled,
enableThrottle = enableThrottle,
role = Role.Button,
onClick = onClick
),
shape = shape,
color = containerColor,
contentColor = contentColor
) {
AppButtonContent(
text = text,
icon = icon,
iconPosition = iconPosition,
textStyle = style,
modifier = iconSpacerModifier
)
}
}
@Composable
private fun AppButtonContent(
text: String,
icon: Painter?,
iconPosition: AppButtonIconPosition,
textStyle: TextStyle,
modifier: Modifier,
) {
val content: @Composable () -> Unit = {
if (icon != null) {
/**
* PNG/JPG/WebP/VectorDrawable:
* painter = painterResource(id = R.drawable.xxx),
* painter = painterResource(id = R.mipmap.xxx)
* Icons.Default:
* painter = rememberVectorPainter(Icons.Default.Xxx)
*/
Icon(painter = icon, contentDescription = null)
Spacer(modifier)
}
Text(text = text, style = textStyle)
}
when {
icon == null -> {
Box(
modifier = Modifier.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
Text(text = text, style = textStyle)
}
}
iconPosition == AppButtonIconPosition.Start -> {
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
content()
}
}
iconPosition == AppButtonIconPosition.Top -> {
Column(
modifier = Modifier.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
content()
}
}
}
}
AppButton.kt 说明:支持文本按钮可带图标:图标位于文本左边或上面。
更多推荐



所有评论(0)