kotlin Compose Indication 的简单实用

507 阅读2分钟

1 Indication 最常见的使用方式是用来解决解决Compose 按钮点击的水波纹效果

private object NoIndication : Indication {
    private object NoIndicationInstance : IndicationInstance {
        override fun ContentDrawScope.drawIndication() {
            drawContent()
        }
    }

    @Composable
    override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
        return NoIndicationInstance
    }
}

 CompositionLocalProvider(
   LocalRippleTheme provides NoRippleTheme,
) {}

这个是最常见的代码 取消按钮点击时候的水波纹效果。

这个水波纹的效果依赖于 indication interactionSource 这两个参数。当我们自己不设置时用的是LocalIndication.current

Modifier.clickable(
    enabled = enabled,
    onClickLabel = onClickLabel,
    onClick = onClick,
    role = role,
    indication = LocalIndication.current,
    interactionSource = remember { MutableInteractionSource() }
)

LocalIndication.current 的默认类型是DefaultDebugIndication

val LocalIndication = staticCompositionLocalOf<Indication> {
    DefaultDebugIndication
}

所以我们想取消某个点击的水波纹效果,只需要把 indication设置为null

      .clickable(
                onClick = {

                },
                interactionSource = remember { MutableInteractionSource() },
                indication = null
            ),

设置为null之后最终使用的Indication就是NoIndication 也就是1中定义的。

val resolvedIndication = indication ?: NoIndication

2 自定义Indication 实现点击效果

这个是官方代码 点击可以实现被点击组件子节点的放大缩小效果。

object ScaleIndication : Indication {
    @Composable
    override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
        // key the remember against interactionSource, so if it changes we create a new instance
        val instance = remember(interactionSource) { ScaleIndicationInstance() }

        LaunchedEffect(interactionSource) {
            interactionSource.interactions.collectLatest { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                    is PressInteraction.Release -> instance.animateToResting()
                    is PressInteraction.Cancel -> instance.animateToResting()
                }
            }
        }

        return instance
    }
}

private class ScaleIndicationInstance : IndicationInstance {
    var currentPressPosition: Offset = Offset.Zero
    val animatedScalePercent = Animatable(1f)

    suspend fun animateToPressed(pressPosition: Offset) {
        currentPressPosition = pressPosition
        animatedScalePercent.animateTo(0.9f, spring())
    }

    suspend fun animateToResting() {
        animatedScalePercent.animateTo(1f, spring())
    }

    override fun ContentDrawScope.drawIndication() {
        scale(
            scale = animatedScalePercent.value,
            pivot = currentPressPosition
        ) {
            this@drawIndication.drawContent()
        }
    }
}

3 跟随NoIndication出现的代码 还有这个样板代码,也是用来改变水波纹的颜色。

object NoRippleTheme : RippleTheme {

    @Composable
    override fun defaultColor(): Color {
        return Color.Red
    }

    @Composable
    override fun rippleAlpha(): RippleAlpha {
        return RippleAlpha(0f, 0f, 0f, 1f)
    }

}

其实使用了 NoIndication 这个 NoRippleTheme就没用了。并且要配合rememberRipple使用RippleTheme设置的属性才能生效。

            .clickable(
                onClick = {

                },
                interactionSource = remember { MutableInteractionSource() },
                indication = rememberRipple()
            ),

image.png

image.png

rememberRipple -> PlatformRipple -> Ripple 在Ripple的rememberUpdatedRippleInstance方法中使用了 LocalRippleTheme.current 也就是我们自己设置的 NoRippleTheme。

总结如下:

1 clickable 如果不设置indication用的就是LocalIndication.current 也就是我们通过 CompositionLocalProvider 设置的 LocalIndication

2 clickable 如果设置 indication ,那么点击效果由自己设置的 indication 决定。

3 如果设置为 null 使用的是 NoIndication,点击没有任何效果。

4 LocalRippleTheme只是一个提供属性的类,至于是否会影响到点击效果,就要看设置的indication中有没有使用它。官方定义的rememberRipple()中就使用到了它,所以改变它会影响rememberRipple()的点击效果。

另外新版本的写法稍有改变:

object ScaleIndicationNodeFactory : IndicationNodeFactory {
    override fun create(interactionSource: InteractionSource): DelegatableNode {
        return ScaleIndicationNode(interactionSource)
    }

    override fun hashCode(): Int = -1

    override fun equals(other: Any?) = other === this
}

class ScaleIndicationNode(
    private val interactionSource: InteractionSource
) : Modifier.Node(), DrawModifierNode {
    var currentPressPosition: Offset = Offset.Zero
    val animatedScalePercent = Animatable(1f)

    private suspend fun animateToPressed(pressPosition: Offset) {
        currentPressPosition = pressPosition
        animatedScalePercent.animateTo(0.9f, spring())
    }

    private suspend fun animateToResting() {
        animatedScalePercent.animateTo(1f, spring())
    }

    override fun onAttach() {
        coroutineScope.launch {
            interactionSource.interactions.collectLatest { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
                    is PressInteraction.Release -> animateToResting()
                    is PressInteraction.Cancel -> animateToResting()
                }
            }
        }
    }

    override fun ContentDrawScope.draw() {
        scale(
            scale = animatedScalePercent.value,
            pivot = currentPressPosition
        ) {
            this@draw.drawContent()
        }
    }
}