DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Ripple/Indication完全ガイド — カスタムリップル/無効化/バウンスエフェクト

この記事で学べること

Ripple/Indication(カスタムリップル、リップル無効化、バウンスエフェクト、カスタムIndication)を解説します。


リップル制御

@Composable
fun RippleExamples() {
    Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
        // デフォルトリップル
        Box(
            Modifier
                .size(100.dp)
                .background(MaterialTheme.colorScheme.primaryContainer)
                .clickable { }
        )

        // カスタムリップル色
        Box(
            Modifier
                .size(100.dp)
                .background(MaterialTheme.colorScheme.secondaryContainer)
                .clickable(
                    interactionSource = remember { MutableInteractionSource() },
                    indication = ripple(color = Color.Red)
                ) { }
        )

        // リップル無効化
        Box(
            Modifier
                .size(100.dp)
                .background(MaterialTheme.colorScheme.tertiaryContainer)
                .clickable(
                    interactionSource = remember { MutableInteractionSource() },
                    indication = null
                ) { }
        )

        // 有界リップル
        Box(
            Modifier
                .size(100.dp)
                .background(Color.LightGray)
                .clickable(
                    interactionSource = remember { MutableInteractionSource() },
                    indication = ripple(bounded = false, radius = 80.dp)
                ) { },
            contentAlignment = Alignment.Center
        ) { Text("無界") }
    }
}
Enter fullscreen mode Exit fullscreen mode

カスタムIndication

class ScaleIndication : IndicationNodeFactory {
    override fun create(interactionSource: InteractionSource): DelegatableNode {
        return ScaleIndicationNode(interactionSource)
    }
}

private class ScaleIndicationNode(
    private val interactionSource: InteractionSource
) : Modifier.Node(), DrawModifierNode {
    private var isPressed = false

    override fun ContentDrawScope.draw() {
        val scale = if (isPressed) 0.95f else 1f
        scale(scale) {
            this@draw.drawContent()
        }
    }

    override fun onAttach() {
        coroutineScope.launch {
            interactionSource.interactions.collect { interaction ->
                isPressed = interaction is PressInteraction.Press
            }
        }
    }
}

// 使用
@Composable
fun ScaleButton(onClick: () -> Unit, content: @Composable () -> Unit) {
    Box(
        Modifier.clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = ScaleIndication()
        ) { onClick() }
    ) { content() }
}
Enter fullscreen mode Exit fullscreen mode

InteractionSource活用

@Composable
fun InteractiveCard(onClick: () -> Unit) {
    val interactionSource = remember { MutableInteractionSource() }
    val isPressed by interactionSource.collectIsPressedAsState()
    val isHovered by interactionSource.collectIsHoveredAsState()

    val elevation by animateDpAsState(
        targetValue = when {
            isPressed -> 1.dp
            isHovered -> 8.dp
            else -> 4.dp
        }, label = "elevation"
    )

    Card(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(interactionSource = interactionSource, indication = ripple()) { onClick() },
        elevation = CardDefaults.cardElevation(defaultElevation = elevation)
    ) {
        Text("インタラクティブカード", Modifier.padding(16.dp))
    }
}
Enter fullscreen mode Exit fullscreen mode

まとめ

API 用途
ripple() Material リップル
indication = null リップル無効化
IndicationNodeFactory カスタムエフェクト
InteractionSource 状態監視
  • ripple()でカスタムカラー/サイズのリップル
  • indication = nullでリップル完全無効化
  • IndicationNodeFactoryでスケール/バウンスエフェクト
  • InteractionSourceでプレス/ホバー状態を監視

8種類のAndroidアプリテンプレート(UIエフェクト対応)を公開しています。

テンプレート一覧Gumroad

関連記事:


I publish 8 Android app templates (Room DB, Material3, MVVM) on Gumroad.

Browse templatesGumroad

Top comments (0)