Jetpack Compose实现无边界圆角矩形水波纹的方法
实现无边界圆角矩形水波纹的解决方案
默认的rememberRipple(bounded = false)会强制使用圆形扩散,要实现围绕Text的无边界圆角矩形波纹,需要自定义Indication来实现自定义形状的波纹绘制。以下是两种可行的方案:
方案一:基于点击位置的圆角矩形扩散波纹
自定义一个支持圆角矩形的无边界波纹Indication,波纹从点击位置向外扩散,保持圆角矩形形状:
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.Indication import androidx.compose.foundation.IndicationInstance import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.ripple.RippleAlpha import androidx.compose.foundation.ripple.RippleTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.RoundedRect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable fun rememberRoundRectUnboundedRipple( color: Color = MaterialTheme.colorScheme.primary, radius: Dp = 8.dp, rippleDuration: Int = 300, rippleAlpha: RippleAlpha = RippleTheme.defaultRippleAlpha(color, lightTheme = isSystemInDarkTheme().not()) ): Indication { return remember(color, radius, rippleDuration, rippleAlpha) { object : Indication { override fun rememberUpdatedState(interactionSource: InteractionSource): IndicationInstance { val activePresses = remember { mutableStateListOf<PressInteraction.Press>() } val scope = rememberCoroutineScope() DisposableEffect(interactionSource) { val subscription = interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { activePresses.add(interaction) scope.launch { delay(rippleDuration.toLong()) activePresses.remove(interaction) } } is PressInteraction.Release, is PressInteraction.Cancel -> { activePresses.removeAll { it == interaction.press } } } } onDispose { subscription.cancel() } } return object : IndicationInstance { override fun ContentDrawScope.drawIndication() { drawContent() val currentPress = activePresses.firstOrNull() ?: return val progress = animateFloatAsState( targetValue = if (activePresses.contains(currentPress)) 1f else 0f, animationSpec = tween(rippleDuration) ).value // 计算无边界扩散的最大尺寸,确保覆盖整个父布局甚至超出 val maxRippleSize = max(size.width, size.height) * 2 val rippleSize = maxRippleSize * progress // 基于点击位置创建圆角矩形波纹区域 val rippleRect = Rect( center = currentPress.pressPosition, radius = rippleSize / 2 ) val roundedRippleRect = RoundedRect( left = rippleRect.left, top = rippleRect.top, right = rippleRect.right, bottom = rippleRect.bottom, cornerRadius = radius.toPx() ) drawRoundRect( color = color.copy(alpha = rippleAlpha.pressedAlpha), topLeft = Offset(roundedRippleRect.left, roundedRippleRect.top), size = Size(roundedRippleRect.width, roundedRippleRect.height), cornerRadius = CornerRadius(radius.toPx()), alpha = progress // 随动画进度调整透明度 ) } } } } } }
使用方式
替换原来的rememberRipple即可:
Text( modifier = Modifier .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRoundRectUnboundedRipple( color = Theme.colors.primary, radius = 8.dp // 设置你需要的圆角大小 ), enabled = buttonEnabled, ) { onDetailClick.invoke() }, text = detailText, color = if (buttonEnabled) Theme.colors.primary else Theme.colors.textDisabled, style = Theme.typography.bodyMediumSemiBold, )
方案二:贴合Text边界的圆角矩形扩散波纹
如果需要波纹从Text的边界向外扩散(而非点击位置),可以先记录Text的尺寸,再基于这个尺寸绘制扩展的圆角矩形波纹:
@Composable fun rememberTextFittedRoundRectRipple( textSize: MutableState<IntSize>, color: Color = MaterialTheme.colorScheme.primary, radius: Dp = 8.dp, expandSize: Dp = 24.dp, rippleDuration: Int = 300, rippleAlpha: RippleAlpha = RippleTheme.defaultRippleAlpha(color, lightTheme = isSystemInDarkTheme().not()) ): Indication { return remember(textSize, color, radius, expandSize, rippleDuration, rippleAlpha) { object : Indication { override fun rememberUpdatedState(interactionSource: InteractionSource): IndicationInstance { val activePresses = remember { mutableStateListOf<PressInteraction.Press>() } val scope = rememberCoroutineScope() DisposableEffect(interactionSource) { val subscription = interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { activePresses.add(interaction) scope.launch { delay(rippleDuration.toLong()) activePresses.remove(interaction) } } is PressInteraction.Release, is PressInteraction.Cancel -> { activePresses.removeAll { it == interaction.press } } } } onDispose { subscription.cancel() } } return object : IndicationInstance { override fun ContentDrawScope.drawIndication() { drawContent() if (activePresses.isEmpty() || textSize.value.isEmpty) return val progress = animateFloatAsState( targetValue = if (activePresses.isNotEmpty()) 1f else 0f, animationSpec = tween(rippleDuration) ).value val textWidth = textSize.value.width.toFloat() val textHeight = textSize.value.height.toFloat() val expandPx = expandSize.toPx() * progress // 计算Text的中心位置,向外扩展波纹 val textLeft = (size.width - textWidth) / 2 - expandPx val textTop = (size.height - textHeight) / 2 - expandPx val textRight = (size.width + textWidth) / 2 + expandPx val textBottom = (size.height + textHeight) / 2 + expandPx drawRoundRect( color = color.copy(alpha = rippleAlpha.pressedAlpha * (1 - progress)), topLeft = Offset(textLeft, textTop), size = Size(textRight - textLeft, textBottom - textTop), cornerRadius = CornerRadius(radius.toPx()), alpha = progress ) } } } } } }
使用方式
需要先通过onSizeChanged记录Text的尺寸:
val textSize = remember { mutableStateOf(IntSize.Zero) } Text( modifier = Modifier .onSizeChanged { textSize.value = it } .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberTextFittedRoundRectRipple( textSize = textSize, color = Theme.colors.primary, radius = 8.dp, expandSize = 24.dp // 设置波纹向外扩展的最大尺寸 ), enabled = buttonEnabled, ) { onDetailClick.invoke() }, text = detailText, color = if (buttonEnabled) Theme.colors.primary else Theme.colors.textDisabled, style = Theme.typography.bodyMediumSemiBold, )
关键说明
- Compose默认的无边界Ripple内部硬编码了圆形形状,因此必须通过自定义
Indication实现自定义波纹形状 - 可以根据需求调整动画时长、圆角大小、扩展尺寸等参数,匹配设计风格
- 两种方案分别适用于"点击位置扩散"和"Text边界扩散"的场景,按需选择
内容的提问来源于stack exchange,提问作者alizeyn
相关产品推荐
相关产品推荐

