summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Joshua Mokut <jmokut@google.com> 2024-09-16 17:45:06 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-09-16 17:45:06 +0000
commitc3b59309957f1b25d2709f7c5e05cdeca57a4cc5 (patch)
tree9b0e343f744361d89a7a915f96ba9aca1313a1e6
parent6214c4ea88b9088c5d582b90467a4ececd34805a (diff)
parent579da54f0936d3c802d228eec8e74939a9627604 (diff)
Merge "Implemented Interaction states for Shortcut Helper" into main
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt152
2 files changed, 170 insertions, 44 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index 11a0543d97d1..4bf552e0f1e3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -286,7 +286,7 @@ private fun CategoryItemSinglePane(
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp)
+ modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp),
) {
ShortcutCategoryIcon(modifier = Modifier.size(24.dp), source = category.icon)
Spacer(modifier = Modifier.width(16.dp))
@@ -717,25 +717,24 @@ private fun CategoryItemTwoPane(
colors: NavigationDrawerItemColors =
NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent),
) {
- val interactionSource = remember { MutableInteractionSource() }
- val isFocused by interactionSource.collectIsFocusedAsState()
-
SelectableShortcutSurface(
selected = selected,
onClick = onClick,
- modifier =
- Modifier.semantics { role = Role.Tab }
- .heightIn(min = 64.dp)
- .fillMaxWidth()
- .outlineFocusModifier(
- isFocused = isFocused,
- focusColor = MaterialTheme.colorScheme.secondary,
- padding = 2.dp,
- cornerRadius = 33.dp,
- ),
+ modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 64.dp).fillMaxWidth(),
shape = RoundedCornerShape(28.dp),
color = colors.containerColor(selected).value,
- interactionSource = interactionSource
+ interactionsConfig =
+ InteractionsConfig(
+ hoverOverlayColor = MaterialTheme.colorScheme.onSurface,
+ hoverOverlayAlpha = 0.11f,
+ pressedOverlayColor = MaterialTheme.colorScheme.onSurface,
+ pressedOverlayAlpha = 0.15f,
+ focusOutlineColor = MaterialTheme.colorScheme.secondary,
+ focusOutlineStrokeWidth = 3.dp,
+ focusOutlinePadding = 2.dp,
+ surfaceCornerRadius = 28.dp,
+ focusOutlineCornerRadius = 33.dp,
+ ),
) {
Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) {
ShortcutCategoryIcon(
@@ -843,25 +842,30 @@ private fun ShortcutsSearchBar(onQueryChange: (String) -> Unit) {
@Composable
private fun KeyboardSettings(horizontalPadding: Dp, verticalPadding: Dp, onClick: () -> Unit) {
val interactionSource = remember { MutableInteractionSource() }
- val isFocused by interactionSource.collectIsFocusedAsState()
ClickableShortcutSurface(
onClick = onClick,
shape = RoundedCornerShape(24.dp),
color = Color.Transparent,
- modifier = Modifier.semantics { role = Role.Button }.fillMaxWidth(),
- interactionSource = interactionSource
+ modifier =
+ Modifier.semantics { role = Role.Button }
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp),
+ interactionSource = interactionSource,
+ interactionsConfig =
+ InteractionsConfig(
+ hoverOverlayColor = MaterialTheme.colorScheme.onSurface,
+ hoverOverlayAlpha = 0.11f,
+ pressedOverlayColor = MaterialTheme.colorScheme.onSurface,
+ pressedOverlayAlpha = 0.15f,
+ focusOutlineColor = MaterialTheme.colorScheme.secondary,
+ focusOutlinePadding = 8.dp,
+ focusOutlineStrokeWidth = 3.dp,
+ surfaceCornerRadius = 24.dp,
+ focusOutlineCornerRadius = 28.dp,
+ hoverPadding = 8.dp,
+ ),
) {
- Row(
- modifier =
- Modifier.padding(horizontal = 12.dp, vertical = 16.dp)
- .outlineFocusModifier(
- isFocused = isFocused,
- focusColor = MaterialTheme.colorScheme.secondary,
- padding = 8.dp,
- cornerRadius = 28.dp,
- ),
- verticalAlignment = Alignment.CenterVertically,
- ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
Text(
"Keyboard Settings",
color = MaterialTheme.colorScheme.onSurfaceVariant,
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt
index 3ba3bd8fdc2f..e49ce6062be3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt
@@ -17,10 +17,16 @@
package com.android.systemui.keyboard.shortcut.ui.composable
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.interaction.FocusInteraction
+import androidx.compose.foundation.interaction.HoverInteraction
+import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.ColorScheme
@@ -35,17 +41,27 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
import com.android.compose.modifiers.thenIf
+import kotlinx.coroutines.launch
/**
* A selectable surface with no default focus/hover indications.
@@ -67,15 +83,17 @@ fun SelectableShortcutSurface(
shadowElevation: Dp = 0.dp,
border: BorderStroke? = null,
interactionSource: MutableInteractionSource? = null,
- content: @Composable () -> Unit
+ interactionsConfig: InteractionsConfig = InteractionsConfig(),
+ content: @Composable () -> Unit,
) {
@Suppress("NAME_SHADOWING")
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
CompositionLocalProvider(
LocalContentColor provides contentColor,
- LocalAbsoluteTonalElevation provides absoluteElevation
+ LocalAbsoluteTonalElevation provides absoluteElevation,
) {
+ val isFocused = interactionSource.collectIsFocusedAsState()
Box(
modifier =
modifier
@@ -85,16 +103,18 @@ fun SelectableShortcutSurface(
backgroundColor =
surfaceColorAtElevation(color = color, elevation = absoluteElevation),
border = border,
- shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }
+ shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() },
)
.selectable(
selected = selected,
interactionSource = interactionSource,
- indication = null,
+ indication =
+ ShortcutHelperIndication(interactionSource, interactionsConfig),
enabled = enabled,
- onClick = onClick
- ),
- propagateMinConstraints = true
+ onClick = onClick,
+ )
+ .thenIf(isFocused.value) { Modifier.zIndex(1f) },
+ propagateMinConstraints = true,
) {
content()
}
@@ -120,14 +140,15 @@ fun ClickableShortcutSurface(
shadowElevation: Dp = 0.dp,
border: BorderStroke? = null,
interactionSource: MutableInteractionSource? = null,
- content: @Composable () -> Unit
+ interactionsConfig: InteractionsConfig = InteractionsConfig(),
+ content: @Composable () -> Unit,
) {
@Suppress("NAME_SHADOWING")
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
CompositionLocalProvider(
LocalContentColor provides contentColor,
- LocalAbsoluteTonalElevation provides absoluteElevation
+ LocalAbsoluteTonalElevation provides absoluteElevation,
) {
Box(
modifier =
@@ -138,15 +159,16 @@ fun ClickableShortcutSurface(
backgroundColor =
surfaceColorAtElevation(color = color, elevation = absoluteElevation),
border = border,
- shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }
+ shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() },
)
.clickable(
interactionSource = interactionSource,
- indication = null,
+ indication =
+ ShortcutHelperIndication(interactionSource, interactionsConfig),
enabled = enabled,
- onClick = onClick
+ onClick = onClick,
),
- propagateMinConstraints = true
+ propagateMinConstraints = true,
) {
content()
}
@@ -195,5 +217,105 @@ private fun Modifier.surface(
}
.thenIf(border != null) { Modifier.border(border!!, shape) }
.background(color = backgroundColor, shape = shape)
- .clip(shape)
}
+
+private class ShortcutHelperInteractionsNode(
+ private val interactionSource: InteractionSource,
+ private val interactionsConfig: InteractionsConfig,
+) : Modifier.Node(), DrawModifierNode {
+
+ var isFocused = mutableStateOf(false)
+ var isHovered = mutableStateOf(false)
+ var isPressed = mutableStateOf(false)
+
+ override fun onAttach() {
+ coroutineScope.launch {
+ val hoverInteractions = mutableListOf<HoverInteraction.Enter>()
+ val focusInteractions = mutableListOf<FocusInteraction.Focus>()
+ val pressInteractions = mutableListOf<PressInteraction.Press>()
+
+ interactionSource.interactions.collect { interaction ->
+ when (interaction) {
+ is FocusInteraction.Focus -> focusInteractions.add(interaction)
+ is FocusInteraction.Unfocus -> focusInteractions.remove(interaction.focus)
+ is HoverInteraction.Enter -> hoverInteractions.add(interaction)
+ is HoverInteraction.Exit -> hoverInteractions.remove(interaction.enter)
+ is PressInteraction.Press -> pressInteractions.add(interaction)
+ is PressInteraction.Release -> pressInteractions.remove(interaction.press)
+ is PressInteraction.Cancel -> pressInteractions.add(interaction.press)
+ }
+ isHovered.value = hoverInteractions.isNotEmpty()
+ isPressed.value = pressInteractions.isNotEmpty()
+ isFocused.value = focusInteractions.isNotEmpty()
+ }
+ }
+ }
+
+ override fun ContentDrawScope.draw() {
+
+ fun getRectangleWithPadding(padding: Dp, size: Size): Rect {
+ return Rect(Offset.Zero, size).let {
+ if (interactionsConfig.focusOutlinePadding > 0.dp) {
+ it.inflate(padding.toPx())
+ } else {
+ it.deflate(padding.unaryMinus().toPx())
+ }
+ }
+ }
+
+ drawContent()
+ if (isHovered.value) {
+ val hoverRect = getRectangleWithPadding(interactionsConfig.pressedPadding, size)
+ drawRoundRect(
+ color = interactionsConfig.hoverOverlayColor,
+ alpha = interactionsConfig.hoverOverlayAlpha,
+ cornerRadius = CornerRadius(interactionsConfig.surfaceCornerRadius.toPx()),
+ topLeft = hoverRect.topLeft,
+ size = hoverRect.size,
+ )
+ }
+ if (isPressed.value) {
+ val pressedRect = getRectangleWithPadding(interactionsConfig.pressedPadding, size)
+ drawRoundRect(
+ color = interactionsConfig.pressedOverlayColor,
+ alpha = interactionsConfig.pressedOverlayAlpha,
+ cornerRadius = CornerRadius(interactionsConfig.surfaceCornerRadius.toPx()),
+ topLeft = pressedRect.topLeft,
+ size = pressedRect.size,
+ )
+ }
+ if (isFocused.value) {
+ val focusOutline = getRectangleWithPadding(interactionsConfig.focusOutlinePadding, size)
+ drawRoundRect(
+ color = interactionsConfig.focusOutlineColor,
+ style = Stroke(width = interactionsConfig.focusOutlineStrokeWidth.toPx()),
+ topLeft = focusOutline.topLeft,
+ size = focusOutline.size,
+ cornerRadius = CornerRadius(interactionsConfig.focusOutlineCornerRadius.toPx()),
+ )
+ }
+ }
+}
+
+data class ShortcutHelperIndication(
+ private val interactionSource: InteractionSource,
+ private val interactionsConfig: InteractionsConfig,
+) : IndicationNodeFactory {
+ override fun create(interactionSource: InteractionSource): DelegatableNode {
+ return ShortcutHelperInteractionsNode(interactionSource, interactionsConfig)
+ }
+}
+
+data class InteractionsConfig(
+ val hoverOverlayColor: Color = Color.Transparent,
+ val hoverOverlayAlpha: Float = 0.0f,
+ val pressedOverlayColor: Color = Color.Transparent,
+ val pressedOverlayAlpha: Float = 0.0f,
+ val focusOutlineColor: Color = Color.Transparent,
+ val focusOutlineStrokeWidth: Dp = 0.dp,
+ val focusOutlinePadding: Dp = 0.dp,
+ val surfaceCornerRadius: Dp = 0.dp,
+ val focusOutlineCornerRadius: Dp = 0.dp,
+ val hoverPadding: Dp = 0.dp,
+ val pressedPadding: Dp = hoverPadding,
+)