You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 00:00:57