From 6080f42ae3e79067b63e0d5db4a34587f43a5686 Mon Sep 17 00:00:00 2001 From: Chris Arriola Date: Tue, 23 Jan 2024 09:31:25 -0800 Subject: [PATCH 1/7] Add ripple snippets. --- compose/snippets/build.gradle.kts | 1 + .../userinteractions/UserInteractions.kt | 141 ++++++++++++++++++ gradle/libs.versions.toml | 3 +- 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index 8c444ac1..bb729778 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -99,6 +99,7 @@ dependencies { implementation(libs.androidx.compose.runtime.livedata) implementation(libs.androidx.compose.materialWindow) implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.compose.material.ripple) implementation(libs.androidx.constraintlayout.compose) implementation(libs.androidx.compose.ui.googlefonts) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt new file mode 100644 index 00000000..c49e5946 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt @@ -0,0 +1,141 @@ +package com.example.compose.snippets.touchinput.userinteractions + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.spring +import androidx.compose.foundation.Indication +import androidx.compose.foundation.IndicationInstance +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Box +import androidx.compose.material.ripple.LocalRippleTheme +import androidx.compose.material.ripple.RippleAlpha +import androidx.compose.material.ripple.RippleTheme +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.scale +import com.example.compose.snippets.architecture.Button +import kotlinx.coroutines.flow.collectLatest + +// [START android_compose_userinteractions_scale_indication] +// [START android_compose_userinteractions_scale_indication_object] +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 + } +} +// [END android_compose_userinteractions_scale_indication_object] + +// [START android_compose_userinteractions_scale_indication_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() + } + } +} +// [END android_compose_userinteractions_scale_indication_instance] +// [END android_compose_userinteractions_scale_indication] + +@Composable +private fun RememberRippleExample() { + // [START android_compose_userinteractions_material_remember_ripple] + Box( + Modifier.clickable( + onClick = {}, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple() + ) + ) { + // ... + } + // [END android_compose_userinteractions_material_remember_ripple] +} + +// [START android_compose_userinteractions_disabled_ripple_theme] +private object DisabledRippleTheme : RippleTheme { + + @Composable + override fun defaultColor(): Color = Color.Transparent + + @Composable + override fun rippleAlpha(): RippleAlpha = RippleAlpha(0f, 0f, 0f, 0f) +} + +// [START_EXCLUDE] +@Composable +private fun MyComposable() { +// [END_EXCLUDE] + CompositionLocalProvider(LocalRippleTheme provides DisabledRippleTheme) { + Button { + // ... + } + } +// [START_EXCLUDE silent] +} +// [END_EXCLUDE] +// [END android_compose_userinteractions_disabled_ripple_theme] + +private val MyRippleAlpha = RippleAlpha(0.5f, 0.5f, 0.5f, 0.5f) + +// [START android_compose_userinteractions_disabled_ripple_theme_color_alpha] +private object DisabledRippleThemeColorAndAlpha : RippleTheme { + + + @Composable + override fun defaultColor(): Color = Color.Red + + @Composable + override fun rippleAlpha(): RippleAlpha = MyRippleAlpha +} + +// [START_EXCLUDE] +@Composable +private fun MyComposable2() { +// [END_EXCLUDE] + CompositionLocalProvider(LocalRippleTheme provides DisabledRippleThemeColorAndAlpha) { + Button { + // ... + } + } +// [START_EXCLUDE silent] +} +// [END_EXCLUDE] +// [END android_compose_userinteractions_disabled_ripple_theme_color_alpha] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2c3788cf..f79bca6b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ junit = "4.13.2" kotlin = "1.9.20" ksp = "1.8.0-1.0.9" maps-compose = "3.1.1" -material = "1.11.0-beta01" +material = "1.11.0" material3-adaptive = "1.0.0-alpha04" material3-adaptive-navigation-suite = "1.0.0-alpha02" # @keep @@ -55,6 +55,7 @@ androidx-compose-foundation = { module = "androidx.compose.foundation:foundation androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } androidx-compose-material = { module = "androidx.compose.material:material" } androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" } +androidx-compose-material-ripple = { module = "androidx.compose.material:material-ripple" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-compose-material3-adaptive = { module = "androidx.compose.material3:material3-adaptive", version.ref = "material3-adaptive" } androidx-compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3-adaptive-navigation-suite" } From f962242bf5d7567df7537ca8b8d3ee7c46fbfbdf Mon Sep 17 00:00:00 2001 From: Chris Arriola Date: Tue, 23 Jan 2024 12:47:25 -0800 Subject: [PATCH 2/7] Add new ripple APIs. --- .../userinteractions/UserInteractions.kt | 106 +++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt index c49e5946..69365eb4 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt @@ -22,8 +22,10 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.node.DrawModifierNode import com.example.compose.snippets.architecture.Button import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch // [START android_compose_userinteractions_scale_indication] // [START android_compose_userinteractions_scale_indication_object] @@ -116,7 +118,7 @@ private fun MyComposable() { private val MyRippleAlpha = RippleAlpha(0.5f, 0.5f, 0.5f, 0.5f) // [START android_compose_userinteractions_disabled_ripple_theme_color_alpha] -private object DisabledRippleThemeColorAndAlpha : RippleTheme { +private object MyRippleTheme : RippleTheme { @Composable @@ -130,7 +132,7 @@ private object DisabledRippleThemeColorAndAlpha : RippleTheme { @Composable private fun MyComposable2() { // [END_EXCLUDE] - CompositionLocalProvider(LocalRippleTheme provides DisabledRippleThemeColorAndAlpha) { + CompositionLocalProvider(LocalRippleTheme provides MyRippleTheme) { Button { // ... } @@ -139,3 +141,103 @@ private fun MyComposable2() { } // [END_EXCLUDE] // [END android_compose_userinteractions_disabled_ripple_theme_color_alpha] + +// Snippets for new ripple API + +// [START android_compose_userinteractions_scale_indication_node_factory] +object ScaleIndicationNodeFactory : IndicationNodeFactory { + override fun create(interactionSource: InteractionSource): DelegatableNode { + return ScaleIndicationNode(interactionSource) + } + + override fun hashCode(): Int = -1 + + override fun equals(other: Any?) = other === this +} +// [END android_compose_userinteractions_scale_indication_node_factory] + +// [START android_compose_userinteractions_scale_indication_node] +private 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() + } + } +} +// [END android_compose_userinteractions_scale_indication_node] + +@Composable +private fun LocalUseFallbackRippleImplementationExample() { +// [START android_compose_userinteractions_localusefallbackrippleimplementation] + CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { + Button { + // ... + } + } +// [END android_compose_userinteractions_localusefallbackrippleimplementation] +} + +// [START android_compose_userinteractions_disabled_ripple_configuration] +private val DisabledRippleConfiguration = + RippleConfiguration(isEnabled = false) + +// [START_EXCLUDE] +@Composable +private fun MyComposableDisabledRippleConfig() { +// [END_EXCLUDE] + CompositionLocalProvider(LocalRippleConfiguration provides DisabledRippleConfiguration) { + Button { + // ... + } + } +// [START_EXCLUDE silent] +} +// [END_EXCLUDE] +// [END android_compose_userinteractions_disabled_ripple_configuration] + +// [START android_compose_userinteractions_my_ripple_configuration] +private val MyRippleConfiguration = + RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha) + +// [START_EXCLUDE] +@Composable +private fun MyComposableMyRippleConfig() { +// [END_EXCLUDE] + CompositionLocalProvider(LocalRippleConfiguration provides MyRippleConfiguration) { + Button { + // ... + } + } +// [START_EXCLUDE silent] +} +// [END_EXCLUDE] +// [END android_compose_userinteractions_my_ripple_configuration] From 974992d743493a9ac0875955e42d78efa636e341 Mon Sep 17 00:00:00 2001 From: Chris Arriola Date: Tue, 23 Jan 2024 13:24:22 -0800 Subject: [PATCH 3/7] Add snippets for interactions. --- .../snippets/touchinput/Interactions.kt | 639 ++++++++++++++++++ 1 file changed, 639 insertions(+) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt new file mode 100644 index 00000000..3aef17b5 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt @@ -0,0 +1,639 @@ +package com.example.compose.snippets.touchinput + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.Interaction +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ShoppingCart +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ButtonElevation +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Brush.Companion.linearGradient +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.sign + +@Composable +private fun InteractionsSnippet1() { + // [START android_compose_interactions_interaction_state] + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + Button( + onClick = { /* do something */ }, + interactionSource = interactionSource) { + Text(if (isPressed) "Pressed!" else "Not pressed") + } + // [END android_compose_interactions_interaction_state] +} + +// [START android_compose_interactions_interaction_source_input] +fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { + // [START_EXCLUDE] + return this + // [END_EXCLUDE] +} +// [END android_compose_interactions_interaction_source_input] + +// [START android_compose_interactions_mutable_interaction_source_input] +fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { + // [START_EXCLUDE] + return this + // [END_EXCLUDE] +} +// [END android_compose_interactions_mutable_interaction_source_input] + +// [START android_compose_interactions_high_level_component] +@Composable +fun Button( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + + // exposes MutableInteractionSource as a parameter + interactionSource: MutableInteractionSource? = null, + + elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), + shape: Shape = MaterialTheme.shapes.small, + border: BorderStroke? = null, + colors: ButtonColors = ButtonDefaults.buttonColors(), + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + content: @Composable RowScope.() -> Unit +) { /* content() */ } +// [END android_compose_interactions_high_level_component] + +@Composable +fun HoverExample() { + // [START android_compose_interactions_hoverable] + // This InteractionSource will emit hover interactions + val interactionSource = remember { MutableInteractionSource() } + + Box( + Modifier + .size(100.dp) + .hoverable(interactionSource = interactionSource), + contentAlignment = Alignment.Center + ) { + Text("Hello!") + } + // [END android_compose_interactions_hoverable] +} + +@Composable +fun FocusableExample() { + // [START android_compose_interactions_focusable] + // This InteractionSource will emit hover and focus interactions + val interactionSource = remember { MutableInteractionSource() } + + Box( + Modifier + .size(100.dp) + .hoverable(interactionSource = interactionSource) + .focusable(interactionSource = interactionSource), + contentAlignment = Alignment.Center + ) { + Text("Hello!") + } + // [END android_compose_interactions_focusable] +} + +@Composable +fun ClickableExample() { + // [START android_compose_interactions_clickable] + // This InteractionSource will emit hover, focus, and press interactions + val interactionSource = remember { MutableInteractionSource() } + Box( + Modifier + .size(100.dp) + .clickable( + onClick = {}, + interactionSource = interactionSource, + + // Also show a ripple effect + indication = rememberRipple() + ), + contentAlignment = Alignment.Center + ) { + Text("Hello!") + } + // [END android_compose_interactions_clickable] +} + +@Composable +private fun InteractionsSnippet2() { + // [START android_compose_interactions_flow_apis] + val interactionSource = remember { MutableInteractionSource() } + val interactions = remember { mutableStateListOf() } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> { + interactions.add(interaction) + } + is DragInteraction.Start -> { + interactions.add(interaction) + } + } + } + } + // [END android_compose_interactions_flow_apis] +} + +@Composable +private fun InteractionsSnippet3() { + // [START android_compose_interactions_add_remove] + val interactionSource = remember { MutableInteractionSource() } + val interactions = remember { mutableStateListOf() } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> { + interactions.add(interaction) + } + is PressInteraction.Release -> { + interactions.remove(interaction.press) + } + is PressInteraction.Cancel -> { + interactions.remove(interaction.press) + } + is DragInteraction.Start -> { + interactions.add(interaction) + } + is DragInteraction.Stop -> { + interactions.remove(interaction.start) + } + is DragInteraction.Cancel -> { + interactions.remove(interaction.start) + } + } + } + } + // [END android_compose_interactions_add_remove] + + // [START android_compose_interactions_is_pressed_or_dragged] + val isPressedOrDragged = interactions.isNotEmpty() + // [END android_compose_interactions_is_pressed_or_dragged] + + // [START android_compose_interactions_last] + val lastInteraction = when (interactions.lastOrNull()) { + is DragInteraction.Start -> "Dragged" + is PressInteraction.Press -> "Pressed" + else -> "No state" + } + // [END android_compose_interactions_last] +} + +@Composable +private fun InteractionsSnippet4() { + // [START android_compose_interactions_batched] + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + Button(onClick = { /* do something */ }, interactionSource = interactionSource) { + Text(if (isPressed) "Pressed!" else "Not pressed") + } + // [END android_compose_interactions_batched] +} + +// [START android_compose_interactions_press_icon_button] +@Composable +fun PressIconButton( + onClick: () -> Unit, + icon: @Composable () -> Unit, + text: @Composable () -> Unit, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = + remember { MutableInteractionSource() }, +) { + val isPressed by interactionSource.collectIsPressedAsState() + Button(onClick = onClick, modifier = modifier, + interactionSource = interactionSource) { + AnimatedVisibility(visible = isPressed) { + if (isPressed) { + Row { + icon() + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + } + } + } + text() + } +} +// [END android_compose_interactions_press_icon_button] + +@Composable +fun PressIconButtonUsage() { +// [START android_compose_interactions_press_icon_button_usage] + PressIconButton( + onClick = {}, + icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, + text = { Text("Add to cart") } + ) +// [END android_compose_interactions_press_icon_button_usage] +} + +@Composable +fun InteractionsSnippet5() { +// [START android_compose_interactions_indication] + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") + + Button( + modifier = Modifier.scale(scale), + onClick = { }, + interactionSource = interactionSource + ) { + Text(if (isPressed) "Pressed!" else "Not pressed") + } +// [END android_compose_interactions_indication] +} + +// [START android_compose_interactions_scale_node] +private class ScaleNode(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() + } + } +} +// [END android_compose_interactions_scale_node] + +// [START android_compose_interactions_scale_node_factory] +object ScaleIndication : IndicationNodeFactory { + override fun create(interactionSource: InteractionSource): DelegatableNode { + return ScaleNode(interactionSource) + } + + override fun equals(other: Any?): Boolean = other === ScaleIndication + override fun hashCode() = 100 +} +// [END android_compose_interactions_scale_node_factory] + +@Composable +fun InteractionSnippets6() { +// [START android_compose_interactions_button_indication] + Box( + modifier = Modifier + .size(100.dp) + .clickable( + onClick = {}, + indication = ScaleIndication, + interactionSource = null + ) + .background(Color.Blue), + contentAlignment = Alignment.Center + ) { + Text("Hello!", color = Color.White) + } +// [END android_compose_interactions_button_indication] +} + +// [START android_compose_interactions_scale_button] +@Composable +fun ScaleButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, + shape: Shape = CircleShape, + content: @Composable RowScope.() -> Unit +) { + Row( + modifier = modifier + .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) + .clickable( + enabled = enabled, + indication = ScaleIndication, + interactionSource = interactionSource, + onClick = onClick + ) + .border(width = 2.dp, color = Color.Blue, shape = shape) + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content + ) +} +// [END android_compose_interactions_scale_button] + +@Composable +fun ScaleButtonExample() { +// [START android_compose_interactions_scale_button_example] + ScaleButton(onClick = {}) { + Icon(Icons.Filled.ShoppingCart, "") + Spacer(Modifier.padding(10.dp)) + Text(text = "Add to cart!") + } +// [END android_compose_interactions_scale_button_example] +} + +// [END android_compose_interactions_neon_node] +private class NeonNode( + private val shape: Shape, + private val borderWidth: Dp, + private val interactionSource: InteractionSource +) : Modifier.Node(), DrawModifierNode { + var currentPressPosition: Offset = Offset.Zero + val animatedProgress = Animatable(0f) + val animatedPressAlpha = Animatable(1f) + + var pressedAnimation: Job? = null + var restingAnimation: Job? = null + + private suspend fun animateToPressed(pressPosition: Offset) { + // Finish any existing animations, in case of a new press while we are still showing + // an animation for a previous one + restingAnimation?.cancel() + pressedAnimation?.cancel() + pressedAnimation = coroutineScope.launch { + currentPressPosition = pressPosition + animatedPressAlpha.snapTo(1f) + animatedProgress.snapTo(0f) + animatedProgress.animateTo(1f, tween(450)) + } + } + + private fun animateToResting() { + restingAnimation = coroutineScope.launch { + // Wait for the existing press animation to finish if it is still ongoing + pressedAnimation?.join() + animatedPressAlpha.animateTo(0f, tween(250)) + animatedProgress.snapTo(0f) + } + } + + override fun onAttach() { + coroutineScope.launch { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> animateToPressed(interaction.pressPosition) + is PressInteraction.Release -> animateToResting() + is PressInteraction.Cancel -> animateToResting() + } + } + } + } + + + override fun ContentDrawScope.draw() { + val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( + currentPressPosition, size + ) + val brush = animateBrush( + startPosition = startPosition, + endPosition = endPosition, + progress = animatedProgress.value + ) + val alpha = animatedPressAlpha.value + + drawContent() + + val outline = shape.createOutline(size, layoutDirection, this) + // Draw overlay on top of content + drawOutline( + outline = outline, + brush = brush, + alpha = alpha * 0.1f + ) + // Draw border on top of overlay + drawOutline( + outline = outline, + brush = brush, + alpha = alpha, + style = Stroke(width = borderWidth.toPx()) + ) + } + + /** + * Calculates a gradient start / end where start is the point on the bounding rectangle of + * size [size] that intercepts with the line drawn from the center to [pressPosition], + * and end is the intercept on the opposite end of that line. + */ + private fun calculateGradientStartAndEndFromPressPosition( + pressPosition: Offset, + size: Size + ): Pair { + // Convert to offset from the center + val offset = pressPosition - size.center + // y = mx + c, c is 0, so just test for x and y to see where the intercept is + val gradient = offset.y / offset.x + // We are starting from the center, so halve the width and height - convert the sign + // to match the offset + val width = (size.width / 2f) * sign(offset.x) + val height = (size.height / 2f) * sign(offset.y) + val x = height / gradient + val y = gradient * width + + // Figure out which intercept lies within bounds + val intercept = if (abs(y) <= abs(height)) { + Offset(width, y) + } else { + Offset(x, height) + } + + // Convert back to offsets from 0,0 + val start = intercept + size.center + val end = Offset(size.width - start.x, size.height - start.y) + return start to end + } + + private fun animateBrush( + startPosition: Offset, + endPosition: Offset, + progress: Float + ): Brush { + if (progress == 0f) return TransparentBrush + + // This is *expensive* - we are doing a lot of allocations on each animation frame. To + // recreate a similar effect in a performant way, it would be better to create one large + // gradient and translate it on each frame, instead of creating a whole new gradient + // and shader. The current approach will be janky! + val colorStops = buildList { + when { + progress < 1/6f -> { + val adjustedProgress = progress * 6f + add(0f to Blue) + add(adjustedProgress to Color.Transparent) + } + progress < 2/6f -> { + val adjustedProgress = (progress - 1/6f) * 6f + add(0f to Purple) + add(adjustedProgress * MaxBlueStop to Blue) + add(adjustedProgress to Blue) + add(1f to Color.Transparent) + } + progress < 3/6f -> { + val adjustedProgress = (progress - 2/6f) * 6f + add(0f to Pink) + add(adjustedProgress * MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + progress < 4/6f -> { + val adjustedProgress = (progress - 3/6f) * 6f + add(0f to Orange) + add(adjustedProgress * MaxPinkStop to Pink) + add(MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + progress < 5/6f -> { + val adjustedProgress = (progress - 4/6f) * 6f + add(0f to Yellow) + add(adjustedProgress * MaxOrangeStop to Orange) + add(MaxPinkStop to Pink) + add(MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + else -> { + val adjustedProgress = (progress - 5/6f) * 6f + add(0f to Yellow) + add(adjustedProgress * MaxYellowStop to Yellow) + add(MaxOrangeStop to Orange) + add(MaxPinkStop to Pink) + add(MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + } + } + + return linearGradient( + colorStops = colorStops.toTypedArray(), + start = startPosition, + end = endPosition + ) + } + + companion object { + val TransparentBrush = SolidColor(Color.Transparent) + val Blue = Color(0xFF30C0D8) + val Purple = Color(0xFF7848A8) + val Pink = Color(0xFFF03078) + val Orange = Color(0xFFF07800) + val Yellow = Color(0xFFF0D800) + const val MaxYellowStop = 0.16f + const val MaxOrangeStop = 0.33f + const val MaxPinkStop = 0.5f + const val MaxPurpleStop = 0.67f + const val MaxBlueStop = 0.83f + } +} +// [END android_compose_interactions_neon_node] + +// [START android_compose_interactions_neon_indication] +class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { + + override fun create(interactionSource: InteractionSource): DelegatableNode { + return NeonNode( + shape, + // Double the border size for a stronger press effect + borderWidth * 2, + interactionSource + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NeonIndication + + if (shape != other.shape) return false + if (borderWidth != other.borderWidth) return false + + return true + } + + override fun hashCode(): Int { + var result = shape.hashCode() + result = 31 * result + borderWidth.hashCode() + return result + } +} +// [END android_compose_interactions_neon_indication] From 569af1463c421fe8680d3773f1ff2e2aac8a8451 Mon Sep 17 00:00:00 2001 From: Chris Arriola Date: Tue, 23 Jan 2024 15:06:47 -0800 Subject: [PATCH 4/7] Added more ripple snippets. --- .../snippets/touchinput/Interactions.kt | 1 + .../userinteractions/UserInteractions.kt | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt index 3aef17b5..278fa308 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt @@ -162,6 +162,7 @@ fun ClickableExample() { interactionSource = interactionSource, // Also show a ripple effect + // Or use ripple() if using version 1.7.0+ indication = rememberRipple() ), contentAlignment = Alignment.Center diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt index 69365eb4..6a25e33c 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt @@ -13,6 +13,7 @@ import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material.ripple.RippleTheme import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -91,6 +92,21 @@ private fun RememberRippleExample() { // [END android_compose_userinteractions_material_remember_ripple] } +// [START android_compose_userinteractions_material_ripple] +@Composable +private fun RippleExample() { + Box( + Modifier.clickable( + onClick = {}, + interactionSource = remember { MutableInteractionSource() }, + indication = ripple() + ) + ) { + // ... + } +} +// [END android_compose_userinteractions_material_ripple] + // [START android_compose_userinteractions_disabled_ripple_theme] private object DisabledRippleTheme : RippleTheme { @@ -195,16 +211,31 @@ private class ScaleIndicationNode( } // [END android_compose_userinteractions_scale_indication_node] +@Composable +fun App() { + +} + @Composable private fun LocalUseFallbackRippleImplementationExample() { // [START android_compose_userinteractions_localusefallbackrippleimplementation] CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { - Button { - // ... + MaterialTheme { + App() } } // [END android_compose_userinteractions_localusefallbackrippleimplementation] + +} + +// [START android_compose_userinteractions_localusefallbackrippleimplementation_app_theme] +@Composable +fun MyAppTheme(content: @Composable () -> Unit) { + CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { + MaterialTheme(content = content) + } } +// [END android_compose_userinteractions_localusefallbackrippleimplementation_app_theme] // [START android_compose_userinteractions_disabled_ripple_configuration] private val DisabledRippleConfiguration = From dedee21106c9c442b7169c63c6019f3a4092cf1c Mon Sep 17 00:00:00 2001 From: Chris Arriola Date: Wed, 24 Jan 2024 10:34:14 -0800 Subject: [PATCH 5/7] Update foundation and material to 1.7.0-alpha01 --- compose/snippets/build.gradle.kts | 1 + .../designsystems/Material2Snippets.kt | 2 +- .../compose/snippets/layouts/PagerSnippets.kt | 2 +- .../snippets/touchinput/Interactions.kt | 20 +++++++++++-------- .../touchinput/focus/FocusSnippets.kt | 1 + .../userinteractions/UserInteractions.kt | 17 ++++++++++++++++ gradle/libs.versions.toml | 9 +++++---- 7 files changed, 38 insertions(+), 14 deletions(-) diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index bb729778..31f953b8 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -82,6 +82,7 @@ dependencies { implementation(composeBom) androidTestImplementation(composeBom) + implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.util) implementation(libs.androidx.compose.ui.graphics) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material2Snippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material2Snippets.kt index f70cd415..540af22e 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material2Snippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material2Snippets.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -@file:Suppress("unused") +@file:Suppress("unused", "DEPRECATION_ERROR") package com.example.compose.snippets.designsystems diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt index f65c96e6..bc73afb1 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt @@ -426,7 +426,7 @@ private fun CustomSnapDistance() { HorizontalPager( state = pagerState, pageSize = PageSize.Fixed(200.dp), - beyondBoundsPageCount = 10, + outOfBoundsPageCount = 10, flingBehavior = fling ) { PagerSampleItem(page = it) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt index 278fa308..010d9dd5 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt @@ -6,6 +6,9 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.IndicationNodeFactory +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.hoverable @@ -27,7 +30,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ShoppingCart -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults @@ -162,8 +165,7 @@ fun ClickableExample() { interactionSource = interactionSource, // Also show a ripple effect - // Or use ripple() if using version 1.7.0+ - indication = rememberRipple() + indication = ripple() ), contentAlignment = Alignment.Center ) { @@ -257,12 +259,14 @@ fun PressIconButton( icon: @Composable () -> Unit, text: @Composable () -> Unit, modifier: Modifier = Modifier, - interactionSource: MutableInteractionSource = - remember { MutableInteractionSource() }, + interactionSource: MutableInteractionSource? = null ) { - val isPressed by interactionSource.collectIsPressedAsState() - Button(onClick = onClick, modifier = modifier, - interactionSource = interactionSource) { + val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false + + Button(onClick = onClick, + modifier = modifier, + interactionSource = interactionSource + ) { AnimatedVisibility(visible = isPressed) { if (isPressed) { Row { diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt index 4a5c7c4c..072eca42 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:Suppress("DEPRECATION_ERROR") package com.example.compose.snippets.touchinput.focus diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt index 6a25e33c..eab9532e 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt @@ -1,14 +1,23 @@ +// This file is intended to show snippets for deprecated API usages +@file:Suppress("DEPRECATION_ERROR") + package com.example.compose.snippets.touchinput.userinteractions import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.spring import androidx.compose.foundation.Indication import androidx.compose.foundation.IndicationInstance +import androidx.compose.foundation.IndicationNodeFactory import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LocalRippleConfiguration +import androidx.compose.material.LocalUseFallbackRippleImplementation +import androidx.compose.material.RippleConfiguration +import androidx.compose.material.ripple import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material.ripple.RippleTheme @@ -23,11 +32,13 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DrawModifierNode import com.example.compose.snippets.architecture.Button import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch + // [START android_compose_userinteractions_scale_indication] // [START android_compose_userinteractions_scale_indication_object] object ScaleIndication : Indication { @@ -216,6 +227,7 @@ fun App() { } +@OptIn(ExperimentalMaterialApi::class) @Composable private fun LocalUseFallbackRippleImplementationExample() { // [START android_compose_userinteractions_localusefallbackrippleimplementation] @@ -229,6 +241,7 @@ private fun LocalUseFallbackRippleImplementationExample() { } // [START android_compose_userinteractions_localusefallbackrippleimplementation_app_theme] +@OptIn(ExperimentalMaterialApi::class) @Composable fun MyAppTheme(content: @Composable () -> Unit) { CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { @@ -238,10 +251,12 @@ fun MyAppTheme(content: @Composable () -> Unit) { // [END android_compose_userinteractions_localusefallbackrippleimplementation_app_theme] // [START android_compose_userinteractions_disabled_ripple_configuration] +@OptIn(ExperimentalMaterialApi::class) private val DisabledRippleConfiguration = RippleConfiguration(isEnabled = false) // [START_EXCLUDE] +@OptIn(ExperimentalMaterialApi::class) @Composable private fun MyComposableDisabledRippleConfig() { // [END_EXCLUDE] @@ -256,10 +271,12 @@ private fun MyComposableDisabledRippleConfig() { // [END android_compose_userinteractions_disabled_ripple_configuration] // [START android_compose_userinteractions_my_ripple_configuration] +@OptIn(ExperimentalMaterialApi::class) private val MyRippleConfiguration = RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha) // [START_EXCLUDE] +@OptIn(ExperimentalMaterialApi::class) @Composable private fun MyComposableMyRippleConfig() { // [END_EXCLUDE] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f79bca6b..f01b12df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,7 @@ coil = "2.4.0" # @keep compileSdk = "34" compose-compiler = "1.5.4" +compose-latest = "1.7.0-alpha01" coroutines = "1.7.3" google-maps = "18.2.0" gradle-versions = "0.49.0" @@ -51,11 +52,11 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } -androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } -androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } -androidx-compose-material = { module = "androidx.compose.material:material" } +androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-latest" } +androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "compose-latest" } +androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "compose-latest" } androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" } -androidx-compose-material-ripple = { module = "androidx.compose.material:material-ripple" } +androidx-compose-material-ripple = { module = "androidx.compose.material:material-ripple", version.ref = "compose-latest" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-compose-material3-adaptive = { module = "androidx.compose.material3:material3-adaptive", version.ref = "material3-adaptive" } androidx-compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3-adaptive-navigation-suite" } From 694bd7dcbce9a710e6b2f3c93fcb0fcdc3f778f2 Mon Sep 17 00:00:00 2001 From: Chris Arriola Date: Wed, 24 Jan 2024 10:40:33 -0800 Subject: [PATCH 6/7] Fix snippet tag. --- .../com/example/compose/snippets/touchinput/Interactions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt index 010d9dd5..bd229a21 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt @@ -416,7 +416,7 @@ fun ScaleButtonExample() { // [END android_compose_interactions_scale_button_example] } -// [END android_compose_interactions_neon_node] +// [START android_compose_interactions_neon_node] private class NeonNode( private val shape: Shape, private val borderWidth: Dp, From ad3517b59c1f51dfe69b7ee712224be3b29b10d6 Mon Sep 17 00:00:00 2001 From: arriolac Date: Wed, 24 Jan 2024 18:42:51 +0000 Subject: [PATCH 7/7] Apply Spotless --- .../snippets/touchinput/Interactions.kt | 907 +++++++++--------- .../touchinput/focus/FocusSnippets.kt | 1 + .../userinteractions/UserInteractions.kt | 155 ++- 3 files changed, 538 insertions(+), 525 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt index bd229a21..0d670060 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.example.compose.snippets.touchinput import androidx.compose.animation.AnimatedVisibility @@ -62,583 +78,584 @@ import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import kotlin.math.abs +import kotlin.math.sign import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlin.math.abs -import kotlin.math.sign @Composable private fun InteractionsSnippet1() { - // [START android_compose_interactions_interaction_state] - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - - Button( - onClick = { /* do something */ }, - interactionSource = interactionSource) { - Text(if (isPressed) "Pressed!" else "Not pressed") - } - // [END android_compose_interactions_interaction_state] + // [START android_compose_interactions_interaction_state] + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + Button( + onClick = { /* do something */ }, + interactionSource = interactionSource + ) { + Text(if (isPressed) "Pressed!" else "Not pressed") + } + // [END android_compose_interactions_interaction_state] } // [START android_compose_interactions_interaction_source_input] fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { - // [START_EXCLUDE] - return this - // [END_EXCLUDE] + // [START_EXCLUDE] + return this + // [END_EXCLUDE] } // [END android_compose_interactions_interaction_source_input] // [START android_compose_interactions_mutable_interaction_source_input] fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { - // [START_EXCLUDE] - return this - // [END_EXCLUDE] + // [START_EXCLUDE] + return this + // [END_EXCLUDE] } // [END android_compose_interactions_mutable_interaction_source_input] // [START android_compose_interactions_high_level_component] @Composable fun Button( - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - - // exposes MutableInteractionSource as a parameter - interactionSource: MutableInteractionSource? = null, - - elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), - shape: Shape = MaterialTheme.shapes.small, - border: BorderStroke? = null, - colors: ButtonColors = ButtonDefaults.buttonColors(), - contentPadding: PaddingValues = ButtonDefaults.ContentPadding, - content: @Composable RowScope.() -> Unit + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + + // exposes MutableInteractionSource as a parameter + interactionSource: MutableInteractionSource? = null, + + elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), + shape: Shape = MaterialTheme.shapes.small, + border: BorderStroke? = null, + colors: ButtonColors = ButtonDefaults.buttonColors(), + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + content: @Composable RowScope.() -> Unit ) { /* content() */ } // [END android_compose_interactions_high_level_component] @Composable fun HoverExample() { - // [START android_compose_interactions_hoverable] - // This InteractionSource will emit hover interactions - val interactionSource = remember { MutableInteractionSource() } - - Box( - Modifier - .size(100.dp) - .hoverable(interactionSource = interactionSource), - contentAlignment = Alignment.Center - ) { - Text("Hello!") - } - // [END android_compose_interactions_hoverable] + // [START android_compose_interactions_hoverable] + // This InteractionSource will emit hover interactions + val interactionSource = remember { MutableInteractionSource() } + + Box( + Modifier + .size(100.dp) + .hoverable(interactionSource = interactionSource), + contentAlignment = Alignment.Center + ) { + Text("Hello!") + } + // [END android_compose_interactions_hoverable] } @Composable fun FocusableExample() { - // [START android_compose_interactions_focusable] - // This InteractionSource will emit hover and focus interactions - val interactionSource = remember { MutableInteractionSource() } - - Box( - Modifier - .size(100.dp) - .hoverable(interactionSource = interactionSource) - .focusable(interactionSource = interactionSource), - contentAlignment = Alignment.Center - ) { - Text("Hello!") - } - // [END android_compose_interactions_focusable] + // [START android_compose_interactions_focusable] + // This InteractionSource will emit hover and focus interactions + val interactionSource = remember { MutableInteractionSource() } + + Box( + Modifier + .size(100.dp) + .hoverable(interactionSource = interactionSource) + .focusable(interactionSource = interactionSource), + contentAlignment = Alignment.Center + ) { + Text("Hello!") + } + // [END android_compose_interactions_focusable] } @Composable fun ClickableExample() { - // [START android_compose_interactions_clickable] - // This InteractionSource will emit hover, focus, and press interactions - val interactionSource = remember { MutableInteractionSource() } - Box( - Modifier - .size(100.dp) - .clickable( - onClick = {}, - interactionSource = interactionSource, - - // Also show a ripple effect - indication = ripple() - ), - contentAlignment = Alignment.Center - ) { - Text("Hello!") - } - // [END android_compose_interactions_clickable] + // [START android_compose_interactions_clickable] + // This InteractionSource will emit hover, focus, and press interactions + val interactionSource = remember { MutableInteractionSource() } + Box( + Modifier + .size(100.dp) + .clickable( + onClick = {}, + interactionSource = interactionSource, + + // Also show a ripple effect + indication = ripple() + ), + contentAlignment = Alignment.Center + ) { + Text("Hello!") + } + // [END android_compose_interactions_clickable] } @Composable private fun InteractionsSnippet2() { - // [START android_compose_interactions_flow_apis] - val interactionSource = remember { MutableInteractionSource() } - val interactions = remember { mutableStateListOf() } - - LaunchedEffect(interactionSource) { - interactionSource.interactions.collect { interaction -> - when (interaction) { - is PressInteraction.Press -> { - interactions.add(interaction) - } - is DragInteraction.Start -> { - interactions.add(interaction) + // [START android_compose_interactions_flow_apis] + val interactionSource = remember { MutableInteractionSource() } + val interactions = remember { mutableStateListOf() } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> { + interactions.add(interaction) + } + is DragInteraction.Start -> { + interactions.add(interaction) + } + } } - } } - } - // [END android_compose_interactions_flow_apis] + // [END android_compose_interactions_flow_apis] } @Composable private fun InteractionsSnippet3() { - // [START android_compose_interactions_add_remove] - val interactionSource = remember { MutableInteractionSource() } - val interactions = remember { mutableStateListOf() } - - LaunchedEffect(interactionSource) { - interactionSource.interactions.collect { interaction -> - when (interaction) { - is PressInteraction.Press -> { - interactions.add(interaction) - } - is PressInteraction.Release -> { - interactions.remove(interaction.press) - } - is PressInteraction.Cancel -> { - interactions.remove(interaction.press) + // [START android_compose_interactions_add_remove] + val interactionSource = remember { MutableInteractionSource() } + val interactions = remember { mutableStateListOf() } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> { + interactions.add(interaction) + } + is PressInteraction.Release -> { + interactions.remove(interaction.press) + } + is PressInteraction.Cancel -> { + interactions.remove(interaction.press) + } + is DragInteraction.Start -> { + interactions.add(interaction) + } + is DragInteraction.Stop -> { + interactions.remove(interaction.start) + } + is DragInteraction.Cancel -> { + interactions.remove(interaction.start) + } + } } - is DragInteraction.Start -> { - interactions.add(interaction) - } - is DragInteraction.Stop -> { - interactions.remove(interaction.start) - } - is DragInteraction.Cancel -> { - interactions.remove(interaction.start) - } - } } - } - // [END android_compose_interactions_add_remove] - - // [START android_compose_interactions_is_pressed_or_dragged] - val isPressedOrDragged = interactions.isNotEmpty() - // [END android_compose_interactions_is_pressed_or_dragged] - - // [START android_compose_interactions_last] - val lastInteraction = when (interactions.lastOrNull()) { - is DragInteraction.Start -> "Dragged" - is PressInteraction.Press -> "Pressed" - else -> "No state" - } - // [END android_compose_interactions_last] + // [END android_compose_interactions_add_remove] + + // [START android_compose_interactions_is_pressed_or_dragged] + val isPressedOrDragged = interactions.isNotEmpty() + // [END android_compose_interactions_is_pressed_or_dragged] + + // [START android_compose_interactions_last] + val lastInteraction = when (interactions.lastOrNull()) { + is DragInteraction.Start -> "Dragged" + is PressInteraction.Press -> "Pressed" + else -> "No state" + } + // [END android_compose_interactions_last] } @Composable private fun InteractionsSnippet4() { - // [START android_compose_interactions_batched] - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - - Button(onClick = { /* do something */ }, interactionSource = interactionSource) { - Text(if (isPressed) "Pressed!" else "Not pressed") - } - // [END android_compose_interactions_batched] + // [START android_compose_interactions_batched] + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + Button(onClick = { /* do something */ }, interactionSource = interactionSource) { + Text(if (isPressed) "Pressed!" else "Not pressed") + } + // [END android_compose_interactions_batched] } // [START android_compose_interactions_press_icon_button] @Composable fun PressIconButton( - onClick: () -> Unit, - icon: @Composable () -> Unit, - text: @Composable () -> Unit, - modifier: Modifier = Modifier, - interactionSource: MutableInteractionSource? = null + onClick: () -> Unit, + icon: @Composable () -> Unit, + text: @Composable () -> Unit, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource? = null ) { - val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false - - Button(onClick = onClick, - modifier = modifier, - interactionSource = interactionSource - ) { - AnimatedVisibility(visible = isPressed) { - if (isPressed) { - Row { - icon() - Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false + + Button( + onClick = onClick, + modifier = modifier, + interactionSource = interactionSource + ) { + AnimatedVisibility(visible = isPressed) { + if (isPressed) { + Row { + icon() + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + } + } } - } + text() } - text() - } } // [END android_compose_interactions_press_icon_button] @Composable fun PressIconButtonUsage() { // [START android_compose_interactions_press_icon_button_usage] - PressIconButton( - onClick = {}, - icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, - text = { Text("Add to cart") } - ) + PressIconButton( + onClick = {}, + icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, + text = { Text("Add to cart") } + ) // [END android_compose_interactions_press_icon_button_usage] } @Composable fun InteractionsSnippet5() { // [START android_compose_interactions_indication] - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") - - Button( - modifier = Modifier.scale(scale), - onClick = { }, - interactionSource = interactionSource - ) { - Text(if (isPressed) "Pressed!" else "Not pressed") - } + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") + + Button( + modifier = Modifier.scale(scale), + onClick = { }, + interactionSource = interactionSource + ) { + Text(if (isPressed) "Pressed!" else "Not pressed") + } // [END android_compose_interactions_indication] } // [START android_compose_interactions_scale_node] private class ScaleNode(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() + 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() + override fun ContentDrawScope.draw() { + scale( + scale = animatedScalePercent.value, + pivot = currentPressPosition + ) { + this@draw.drawContent() + } } - } } // [END android_compose_interactions_scale_node] // [START android_compose_interactions_scale_node_factory] object ScaleIndication : IndicationNodeFactory { - override fun create(interactionSource: InteractionSource): DelegatableNode { - return ScaleNode(interactionSource) - } + override fun create(interactionSource: InteractionSource): DelegatableNode { + return ScaleNode(interactionSource) + } - override fun equals(other: Any?): Boolean = other === ScaleIndication - override fun hashCode() = 100 + override fun equals(other: Any?): Boolean = other === ScaleIndication + override fun hashCode() = 100 } // [END android_compose_interactions_scale_node_factory] @Composable fun InteractionSnippets6() { // [START android_compose_interactions_button_indication] - Box( - modifier = Modifier - .size(100.dp) - .clickable( - onClick = {}, - indication = ScaleIndication, - interactionSource = null - ) - .background(Color.Blue), - contentAlignment = Alignment.Center - ) { - Text("Hello!", color = Color.White) - } + Box( + modifier = Modifier + .size(100.dp) + .clickable( + onClick = {}, + indication = ScaleIndication, + interactionSource = null + ) + .background(Color.Blue), + contentAlignment = Alignment.Center + ) { + Text("Hello!", color = Color.White) + } // [END android_compose_interactions_button_indication] } // [START android_compose_interactions_scale_button] @Composable fun ScaleButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - interactionSource: MutableInteractionSource? = null, - shape: Shape = CircleShape, - content: @Composable RowScope.() -> Unit + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, + shape: Shape = CircleShape, + content: @Composable RowScope.() -> Unit ) { - Row( - modifier = modifier - .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) - .clickable( - enabled = enabled, - indication = ScaleIndication, - interactionSource = interactionSource, - onClick = onClick - ) - .border(width = 2.dp, color = Color.Blue, shape = shape) - .padding(horizontal = 16.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - content = content - ) + Row( + modifier = modifier + .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) + .clickable( + enabled = enabled, + indication = ScaleIndication, + interactionSource = interactionSource, + onClick = onClick + ) + .border(width = 2.dp, color = Color.Blue, shape = shape) + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content + ) } // [END android_compose_interactions_scale_button] @Composable fun ScaleButtonExample() { // [START android_compose_interactions_scale_button_example] - ScaleButton(onClick = {}) { - Icon(Icons.Filled.ShoppingCart, "") - Spacer(Modifier.padding(10.dp)) - Text(text = "Add to cart!") - } + ScaleButton(onClick = {}) { + Icon(Icons.Filled.ShoppingCart, "") + Spacer(Modifier.padding(10.dp)) + Text(text = "Add to cart!") + } // [END android_compose_interactions_scale_button_example] } // [START android_compose_interactions_neon_node] private class NeonNode( - private val shape: Shape, - private val borderWidth: Dp, - private val interactionSource: InteractionSource + private val shape: Shape, + private val borderWidth: Dp, + private val interactionSource: InteractionSource ) : Modifier.Node(), DrawModifierNode { - var currentPressPosition: Offset = Offset.Zero - val animatedProgress = Animatable(0f) - val animatedPressAlpha = Animatable(1f) - - var pressedAnimation: Job? = null - var restingAnimation: Job? = null - - private suspend fun animateToPressed(pressPosition: Offset) { - // Finish any existing animations, in case of a new press while we are still showing - // an animation for a previous one - restingAnimation?.cancel() - pressedAnimation?.cancel() - pressedAnimation = coroutineScope.launch { - currentPressPosition = pressPosition - animatedPressAlpha.snapTo(1f) - animatedProgress.snapTo(0f) - animatedProgress.animateTo(1f, tween(450)) - } - } - - private fun animateToResting() { - restingAnimation = coroutineScope.launch { - // Wait for the existing press animation to finish if it is still ongoing - pressedAnimation?.join() - animatedPressAlpha.animateTo(0f, tween(250)) - animatedProgress.snapTo(0f) + var currentPressPosition: Offset = Offset.Zero + val animatedProgress = Animatable(0f) + val animatedPressAlpha = Animatable(1f) + + var pressedAnimation: Job? = null + var restingAnimation: Job? = null + + private suspend fun animateToPressed(pressPosition: Offset) { + // Finish any existing animations, in case of a new press while we are still showing + // an animation for a previous one + restingAnimation?.cancel() + pressedAnimation?.cancel() + pressedAnimation = coroutineScope.launch { + currentPressPosition = pressPosition + animatedPressAlpha.snapTo(1f) + animatedProgress.snapTo(0f) + animatedProgress.animateTo(1f, tween(450)) + } } - } - - override fun onAttach() { - coroutineScope.launch { - interactionSource.interactions.collect { interaction -> - when (interaction) { - is PressInteraction.Press -> animateToPressed(interaction.pressPosition) - is PressInteraction.Release -> animateToResting() - is PressInteraction.Cancel -> animateToResting() + + private fun animateToResting() { + restingAnimation = coroutineScope.launch { + // Wait for the existing press animation to finish if it is still ongoing + pressedAnimation?.join() + animatedPressAlpha.animateTo(0f, tween(250)) + animatedProgress.snapTo(0f) } - } } - } + override fun onAttach() { + coroutineScope.launch { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> animateToPressed(interaction.pressPosition) + is PressInteraction.Release -> animateToResting() + is PressInteraction.Cancel -> animateToResting() + } + } + } + } - override fun ContentDrawScope.draw() { - val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( - currentPressPosition, size - ) - val brush = animateBrush( - startPosition = startPosition, - endPosition = endPosition, - progress = animatedProgress.value - ) - val alpha = animatedPressAlpha.value + override fun ContentDrawScope.draw() { + val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( + currentPressPosition, size + ) + val brush = animateBrush( + startPosition = startPosition, + endPosition = endPosition, + progress = animatedProgress.value + ) + val alpha = animatedPressAlpha.value + + drawContent() + + val outline = shape.createOutline(size, layoutDirection, this) + // Draw overlay on top of content + drawOutline( + outline = outline, + brush = brush, + alpha = alpha * 0.1f + ) + // Draw border on top of overlay + drawOutline( + outline = outline, + brush = brush, + alpha = alpha, + style = Stroke(width = borderWidth.toPx()) + ) + } - drawContent() + /** + * Calculates a gradient start / end where start is the point on the bounding rectangle of + * size [size] that intercepts with the line drawn from the center to [pressPosition], + * and end is the intercept on the opposite end of that line. + */ + private fun calculateGradientStartAndEndFromPressPosition( + pressPosition: Offset, + size: Size + ): Pair { + // Convert to offset from the center + val offset = pressPosition - size.center + // y = mx + c, c is 0, so just test for x and y to see where the intercept is + val gradient = offset.y / offset.x + // We are starting from the center, so halve the width and height - convert the sign + // to match the offset + val width = (size.width / 2f) * sign(offset.x) + val height = (size.height / 2f) * sign(offset.y) + val x = height / gradient + val y = gradient * width + + // Figure out which intercept lies within bounds + val intercept = if (abs(y) <= abs(height)) { + Offset(width, y) + } else { + Offset(x, height) + } - val outline = shape.createOutline(size, layoutDirection, this) - // Draw overlay on top of content - drawOutline( - outline = outline, - brush = brush, - alpha = alpha * 0.1f - ) - // Draw border on top of overlay - drawOutline( - outline = outline, - brush = brush, - alpha = alpha, - style = Stroke(width = borderWidth.toPx()) - ) - } - - /** - * Calculates a gradient start / end where start is the point on the bounding rectangle of - * size [size] that intercepts with the line drawn from the center to [pressPosition], - * and end is the intercept on the opposite end of that line. - */ - private fun calculateGradientStartAndEndFromPressPosition( - pressPosition: Offset, - size: Size - ): Pair { - // Convert to offset from the center - val offset = pressPosition - size.center - // y = mx + c, c is 0, so just test for x and y to see where the intercept is - val gradient = offset.y / offset.x - // We are starting from the center, so halve the width and height - convert the sign - // to match the offset - val width = (size.width / 2f) * sign(offset.x) - val height = (size.height / 2f) * sign(offset.y) - val x = height / gradient - val y = gradient * width - - // Figure out which intercept lies within bounds - val intercept = if (abs(y) <= abs(height)) { - Offset(width, y) - } else { - Offset(x, height) + // Convert back to offsets from 0,0 + val start = intercept + size.center + val end = Offset(size.width - start.x, size.height - start.y) + return start to end } - // Convert back to offsets from 0,0 - val start = intercept + size.center - val end = Offset(size.width - start.x, size.height - start.y) - return start to end - } - - private fun animateBrush( - startPosition: Offset, - endPosition: Offset, - progress: Float - ): Brush { - if (progress == 0f) return TransparentBrush - - // This is *expensive* - we are doing a lot of allocations on each animation frame. To - // recreate a similar effect in a performant way, it would be better to create one large - // gradient and translate it on each frame, instead of creating a whole new gradient - // and shader. The current approach will be janky! - val colorStops = buildList { - when { - progress < 1/6f -> { - val adjustedProgress = progress * 6f - add(0f to Blue) - add(adjustedProgress to Color.Transparent) - } - progress < 2/6f -> { - val adjustedProgress = (progress - 1/6f) * 6f - add(0f to Purple) - add(adjustedProgress * MaxBlueStop to Blue) - add(adjustedProgress to Blue) - add(1f to Color.Transparent) - } - progress < 3/6f -> { - val adjustedProgress = (progress - 2/6f) * 6f - add(0f to Pink) - add(adjustedProgress * MaxPurpleStop to Purple) - add(MaxBlueStop to Blue) - add(1f to Blue) - } - progress < 4/6f -> { - val adjustedProgress = (progress - 3/6f) * 6f - add(0f to Orange) - add(adjustedProgress * MaxPinkStop to Pink) - add(MaxPurpleStop to Purple) - add(MaxBlueStop to Blue) - add(1f to Blue) - } - progress < 5/6f -> { - val adjustedProgress = (progress - 4/6f) * 6f - add(0f to Yellow) - add(adjustedProgress * MaxOrangeStop to Orange) - add(MaxPinkStop to Pink) - add(MaxPurpleStop to Purple) - add(MaxBlueStop to Blue) - add(1f to Blue) + private fun animateBrush( + startPosition: Offset, + endPosition: Offset, + progress: Float + ): Brush { + if (progress == 0f) return TransparentBrush + + // This is *expensive* - we are doing a lot of allocations on each animation frame. To + // recreate a similar effect in a performant way, it would be better to create one large + // gradient and translate it on each frame, instead of creating a whole new gradient + // and shader. The current approach will be janky! + val colorStops = buildList { + when { + progress < 1 / 6f -> { + val adjustedProgress = progress * 6f + add(0f to Blue) + add(adjustedProgress to Color.Transparent) + } + progress < 2 / 6f -> { + val adjustedProgress = (progress - 1 / 6f) * 6f + add(0f to Purple) + add(adjustedProgress * MaxBlueStop to Blue) + add(adjustedProgress to Blue) + add(1f to Color.Transparent) + } + progress < 3 / 6f -> { + val adjustedProgress = (progress - 2 / 6f) * 6f + add(0f to Pink) + add(adjustedProgress * MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + progress < 4 / 6f -> { + val adjustedProgress = (progress - 3 / 6f) * 6f + add(0f to Orange) + add(adjustedProgress * MaxPinkStop to Pink) + add(MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + progress < 5 / 6f -> { + val adjustedProgress = (progress - 4 / 6f) * 6f + add(0f to Yellow) + add(adjustedProgress * MaxOrangeStop to Orange) + add(MaxPinkStop to Pink) + add(MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + else -> { + val adjustedProgress = (progress - 5 / 6f) * 6f + add(0f to Yellow) + add(adjustedProgress * MaxYellowStop to Yellow) + add(MaxOrangeStop to Orange) + add(MaxPinkStop to Pink) + add(MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + } } - else -> { - val adjustedProgress = (progress - 5/6f) * 6f - add(0f to Yellow) - add(adjustedProgress * MaxYellowStop to Yellow) - add(MaxOrangeStop to Orange) - add(MaxPinkStop to Pink) - add(MaxPurpleStop to Purple) - add(MaxBlueStop to Blue) - add(1f to Blue) - } - } + + return linearGradient( + colorStops = colorStops.toTypedArray(), + start = startPosition, + end = endPosition + ) } - return linearGradient( - colorStops = colorStops.toTypedArray(), - start = startPosition, - end = endPosition - ) - } - - companion object { - val TransparentBrush = SolidColor(Color.Transparent) - val Blue = Color(0xFF30C0D8) - val Purple = Color(0xFF7848A8) - val Pink = Color(0xFFF03078) - val Orange = Color(0xFFF07800) - val Yellow = Color(0xFFF0D800) - const val MaxYellowStop = 0.16f - const val MaxOrangeStop = 0.33f - const val MaxPinkStop = 0.5f - const val MaxPurpleStop = 0.67f - const val MaxBlueStop = 0.83f - } + companion object { + val TransparentBrush = SolidColor(Color.Transparent) + val Blue = Color(0xFF30C0D8) + val Purple = Color(0xFF7848A8) + val Pink = Color(0xFFF03078) + val Orange = Color(0xFFF07800) + val Yellow = Color(0xFFF0D800) + const val MaxYellowStop = 0.16f + const val MaxOrangeStop = 0.33f + const val MaxPinkStop = 0.5f + const val MaxPurpleStop = 0.67f + const val MaxBlueStop = 0.83f + } } // [END android_compose_interactions_neon_node] // [START android_compose_interactions_neon_indication] class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { - override fun create(interactionSource: InteractionSource): DelegatableNode { - return NeonNode( - shape, - // Double the border size for a stronger press effect - borderWidth * 2, - interactionSource - ) - } + override fun create(interactionSource: InteractionSource): DelegatableNode { + return NeonNode( + shape, + // Double the border size for a stronger press effect + borderWidth * 2, + interactionSource + ) + } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false - other as NeonIndication + other as NeonIndication - if (shape != other.shape) return false - if (borderWidth != other.borderWidth) return false + if (shape != other.shape) return false + if (borderWidth != other.borderWidth) return false - return true - } + return true + } - override fun hashCode(): Int { - var result = shape.hashCode() - result = 31 * result + borderWidth.hashCode() - return result - } + override fun hashCode(): Int { + var result = shape.hashCode() + result = 31 * result + borderWidth.hashCode() + return result + } } // [END android_compose_interactions_neon_indication] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt index 072eca42..2391d48b 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + @file:Suppress("DEPRECATION_ERROR") package com.example.compose.snippets.touchinput.focus diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt index ae888656..e34dfaa6 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt @@ -14,7 +14,6 @@ * limitations under the License. */ -// This file is intended to show snippets for deprecated API usages @file:Suppress("DEPRECATION_ERROR") package com.example.compose.snippets.touchinput.userinteractions @@ -53,8 +52,6 @@ import androidx.compose.ui.node.DrawModifierNode import com.example.compose.snippets.architecture.Button import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import com.example.compose.snippets.architecture.Button -import kotlinx.coroutines.flow.collectLatest // [START android_compose_userinteractions_scale_indication] // [START android_compose_userinteractions_scale_indication_object] @@ -107,42 +104,42 @@ private class ScaleIndicationInstance : IndicationInstance { @Composable private fun RememberRippleExample() { - // [START android_compose_userinteractions_material_remember_ripple] - Box( - Modifier.clickable( - onClick = {}, - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple() - ) - ) { - // ... - } - // [END android_compose_userinteractions_material_remember_ripple] + // [START android_compose_userinteractions_material_remember_ripple] + Box( + Modifier.clickable( + onClick = {}, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple() + ) + ) { + // ... + } + // [END android_compose_userinteractions_material_remember_ripple] } // [START android_compose_userinteractions_material_ripple] @Composable private fun RippleExample() { - Box( - Modifier.clickable( - onClick = {}, - interactionSource = remember { MutableInteractionSource() }, - indication = ripple() - ) - ) { - // ... - } + Box( + Modifier.clickable( + onClick = {}, + interactionSource = remember { MutableInteractionSource() }, + indication = ripple() + ) + ) { + // ... + } } // [END android_compose_userinteractions_material_ripple] // [START android_compose_userinteractions_disabled_ripple_theme] private object DisabledRippleTheme : RippleTheme { - @Composable - override fun defaultColor(): Color = Color.Transparent + @Composable + override fun defaultColor(): Color = Color.Transparent - @Composable - override fun rippleAlpha(): RippleAlpha = RippleAlpha(0f, 0f, 0f, 0f) + @Composable + override fun rippleAlpha(): RippleAlpha = RippleAlpha(0f, 0f, 0f, 0f) } // [START_EXCLUDE] @@ -189,98 +186,96 @@ private fun MyComposable2() { // [START android_compose_userinteractions_scale_indication_node_factory] object ScaleIndicationNodeFactory : IndicationNodeFactory { - override fun create(interactionSource: InteractionSource): DelegatableNode { - return ScaleIndicationNode(interactionSource) - } + override fun create(interactionSource: InteractionSource): DelegatableNode { + return ScaleIndicationNode(interactionSource) + } - override fun hashCode(): Int = -1 + override fun hashCode(): Int = -1 - override fun equals(other: Any?) = other === this + override fun equals(other: Any?) = other === this } // [END android_compose_userinteractions_scale_indication_node_factory] // [START android_compose_userinteractions_scale_indication_node] private class ScaleIndicationNode( - private val interactionSource: InteractionSource + 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() + 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() + override fun ContentDrawScope.draw() { + scale( + scale = animatedScalePercent.value, + pivot = currentPressPosition + ) { + this@draw.drawContent() + } } - } } // [END android_compose_userinteractions_scale_indication_node] @Composable fun App() { - } @OptIn(ExperimentalMaterialApi::class) @Composable private fun LocalUseFallbackRippleImplementationExample() { // [START android_compose_userinteractions_localusefallbackrippleimplementation] - CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { - MaterialTheme { - App() + CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { + MaterialTheme { + App() + } } - } // [END android_compose_userinteractions_localusefallbackrippleimplementation] - } // [START android_compose_userinteractions_localusefallbackrippleimplementation_app_theme] @OptIn(ExperimentalMaterialApi::class) @Composable fun MyAppTheme(content: @Composable () -> Unit) { - CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { - MaterialTheme(content = content) - } + CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { + MaterialTheme(content = content) + } } // [END android_compose_userinteractions_localusefallbackrippleimplementation_app_theme] // [START android_compose_userinteractions_disabled_ripple_configuration] @OptIn(ExperimentalMaterialApi::class) private val DisabledRippleConfiguration = - RippleConfiguration(isEnabled = false) + RippleConfiguration(isEnabled = false) // [START_EXCLUDE] @OptIn(ExperimentalMaterialApi::class) @Composable private fun MyComposableDisabledRippleConfig() { // [END_EXCLUDE] - CompositionLocalProvider(LocalRippleConfiguration provides DisabledRippleConfiguration) { - Button { - // ... + CompositionLocalProvider(LocalRippleConfiguration provides DisabledRippleConfiguration) { + Button { + // ... + } } - } // [START_EXCLUDE silent] } // [END_EXCLUDE] @@ -289,18 +284,18 @@ private fun MyComposableDisabledRippleConfig() { // [START android_compose_userinteractions_my_ripple_configuration] @OptIn(ExperimentalMaterialApi::class) private val MyRippleConfiguration = - RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha) + RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha) // [START_EXCLUDE] @OptIn(ExperimentalMaterialApi::class) @Composable private fun MyComposableMyRippleConfig() { // [END_EXCLUDE] - CompositionLocalProvider(LocalRippleConfiguration provides MyRippleConfiguration) { - Button { - // ... + CompositionLocalProvider(LocalRippleConfiguration provides MyRippleConfiguration) { + Button { + // ... + } } - } // [START_EXCLUDE silent] } // [END_EXCLUDE]