Loading packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt +33 −29 Original line number Original line Diff line number Diff line Loading @@ -286,7 +286,7 @@ private fun CategoryItemSinglePane( Column { Column { Row( Row( verticalAlignment = Alignment.CenterVertically, 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) ShortcutCategoryIcon(modifier = Modifier.size(24.dp), source = category.icon) Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp)) Loading Loading @@ -717,25 +717,24 @@ private fun CategoryItemTwoPane( colors: NavigationDrawerItemColors = colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent), NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent), ) { ) { val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() SelectableShortcutSurface( SelectableShortcutSurface( selected = selected, selected = selected, onClick = onClick, onClick = onClick, modifier = modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 64.dp).fillMaxWidth(), Modifier.semantics { role = Role.Tab } .heightIn(min = 64.dp) .fillMaxWidth() .outlineFocusModifier( isFocused = isFocused, focusColor = MaterialTheme.colorScheme.secondary, padding = 2.dp, cornerRadius = 33.dp, ), shape = RoundedCornerShape(28.dp), shape = RoundedCornerShape(28.dp), color = colors.containerColor(selected).value, 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) { Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) { ShortcutCategoryIcon( ShortcutCategoryIcon( Loading Loading @@ -843,25 +842,30 @@ private fun ShortcutsSearchBar(onQueryChange: (String) -> Unit) { @Composable @Composable private fun KeyboardSettings(horizontalPadding: Dp, verticalPadding: Dp, onClick: () -> Unit) { private fun KeyboardSettings(horizontalPadding: Dp, verticalPadding: Dp, onClick: () -> Unit) { val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() ClickableShortcutSurface( ClickableShortcutSurface( onClick = onClick, onClick = onClick, shape = RoundedCornerShape(24.dp), shape = RoundedCornerShape(24.dp), color = Color.Transparent, color = Color.Transparent, modifier = Modifier.semantics { role = Role.Button }.fillMaxWidth(), interactionSource = interactionSource ) { Row( modifier = modifier = Modifier.padding(horizontal = 12.dp, vertical = 16.dp) Modifier.semantics { role = Role.Button } .outlineFocusModifier( .fillMaxWidth() isFocused = isFocused, .padding(horizontal = 12.dp), focusColor = MaterialTheme.colorScheme.secondary, interactionSource = interactionSource, padding = 8.dp, interactionsConfig = cornerRadius = 28.dp, 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, ), ), verticalAlignment = Alignment.CenterVertically, ) { ) { Row(verticalAlignment = Alignment.CenterVertically) { Text( Text( "Keyboard Settings", "Keyboard Settings", color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant, Loading packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt +137 −15 Original line number Original line Diff line number Diff line Loading @@ -17,10 +17,16 @@ package com.android.systemui.keyboard.shortcut.ui.composable package com.android.systemui.keyboard.shortcut.ui.composable import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.IndicationNodeFactory import androidx.compose.foundation.background import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.border import androidx.compose.foundation.clickable 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.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme Loading @@ -35,17 +41,27 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.remember import androidx.compose.ui.Modifier 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.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape 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.graphics.graphicsLayer import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import com.android.compose.modifiers.thenIf import com.android.compose.modifiers.thenIf import kotlinx.coroutines.launch /** /** * A selectable surface with no default focus/hover indications. * A selectable surface with no default focus/hover indications. Loading @@ -67,15 +83,17 @@ fun SelectableShortcutSurface( shadowElevation: Dp = 0.dp, shadowElevation: Dp = 0.dp, border: BorderStroke? = null, border: BorderStroke? = null, interactionSource: MutableInteractionSource? = null, interactionSource: MutableInteractionSource? = null, content: @Composable () -> Unit interactionsConfig: InteractionsConfig = InteractionsConfig(), content: @Composable () -> Unit, ) { ) { @Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation CompositionLocalProvider( CompositionLocalProvider( LocalContentColor provides contentColor, LocalContentColor provides contentColor, LocalAbsoluteTonalElevation provides absoluteElevation LocalAbsoluteTonalElevation provides absoluteElevation, ) { ) { val isFocused = interactionSource.collectIsFocusedAsState() Box( Box( modifier = modifier = modifier modifier Loading @@ -85,16 +103,18 @@ fun SelectableShortcutSurface( backgroundColor = backgroundColor = surfaceColorAtElevation(color = color, elevation = absoluteElevation), surfaceColorAtElevation(color = color, elevation = absoluteElevation), border = border, border = border, shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() } shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }, ) ) .selectable( .selectable( selected = selected, selected = selected, interactionSource = interactionSource, interactionSource = interactionSource, indication = null, indication = ShortcutHelperIndication(interactionSource, interactionsConfig), enabled = enabled, enabled = enabled, onClick = onClick onClick = onClick, ), ) propagateMinConstraints = true .thenIf(isFocused.value) { Modifier.zIndex(1f) }, propagateMinConstraints = true, ) { ) { content() content() } } Loading @@ -120,14 +140,15 @@ fun ClickableShortcutSurface( shadowElevation: Dp = 0.dp, shadowElevation: Dp = 0.dp, border: BorderStroke? = null, border: BorderStroke? = null, interactionSource: MutableInteractionSource? = null, interactionSource: MutableInteractionSource? = null, content: @Composable () -> Unit interactionsConfig: InteractionsConfig = InteractionsConfig(), content: @Composable () -> Unit, ) { ) { @Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation CompositionLocalProvider( CompositionLocalProvider( LocalContentColor provides contentColor, LocalContentColor provides contentColor, LocalAbsoluteTonalElevation provides absoluteElevation LocalAbsoluteTonalElevation provides absoluteElevation, ) { ) { Box( Box( modifier = modifier = Loading @@ -138,15 +159,16 @@ fun ClickableShortcutSurface( backgroundColor = backgroundColor = surfaceColorAtElevation(color = color, elevation = absoluteElevation), surfaceColorAtElevation(color = color, elevation = absoluteElevation), border = border, border = border, shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() } shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }, ) ) .clickable( .clickable( interactionSource = interactionSource, interactionSource = interactionSource, indication = null, indication = ShortcutHelperIndication(interactionSource, interactionsConfig), enabled = enabled, enabled = enabled, onClick = onClick onClick = onClick, ), ), propagateMinConstraints = true propagateMinConstraints = true, ) { ) { content() content() } } Loading Loading @@ -195,5 +217,105 @@ private fun Modifier.surface( } } .thenIf(border != null) { Modifier.border(border!!, shape) } .thenIf(border != null) { Modifier.border(border!!, shape) } .background(color = backgroundColor, shape = 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, ) Loading
packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt +33 −29 Original line number Original line Diff line number Diff line Loading @@ -286,7 +286,7 @@ private fun CategoryItemSinglePane( Column { Column { Row( Row( verticalAlignment = Alignment.CenterVertically, 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) ShortcutCategoryIcon(modifier = Modifier.size(24.dp), source = category.icon) Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp)) Loading Loading @@ -717,25 +717,24 @@ private fun CategoryItemTwoPane( colors: NavigationDrawerItemColors = colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent), NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent), ) { ) { val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() SelectableShortcutSurface( SelectableShortcutSurface( selected = selected, selected = selected, onClick = onClick, onClick = onClick, modifier = modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 64.dp).fillMaxWidth(), Modifier.semantics { role = Role.Tab } .heightIn(min = 64.dp) .fillMaxWidth() .outlineFocusModifier( isFocused = isFocused, focusColor = MaterialTheme.colorScheme.secondary, padding = 2.dp, cornerRadius = 33.dp, ), shape = RoundedCornerShape(28.dp), shape = RoundedCornerShape(28.dp), color = colors.containerColor(selected).value, 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) { Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) { ShortcutCategoryIcon( ShortcutCategoryIcon( Loading Loading @@ -843,25 +842,30 @@ private fun ShortcutsSearchBar(onQueryChange: (String) -> Unit) { @Composable @Composable private fun KeyboardSettings(horizontalPadding: Dp, verticalPadding: Dp, onClick: () -> Unit) { private fun KeyboardSettings(horizontalPadding: Dp, verticalPadding: Dp, onClick: () -> Unit) { val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() ClickableShortcutSurface( ClickableShortcutSurface( onClick = onClick, onClick = onClick, shape = RoundedCornerShape(24.dp), shape = RoundedCornerShape(24.dp), color = Color.Transparent, color = Color.Transparent, modifier = Modifier.semantics { role = Role.Button }.fillMaxWidth(), interactionSource = interactionSource ) { Row( modifier = modifier = Modifier.padding(horizontal = 12.dp, vertical = 16.dp) Modifier.semantics { role = Role.Button } .outlineFocusModifier( .fillMaxWidth() isFocused = isFocused, .padding(horizontal = 12.dp), focusColor = MaterialTheme.colorScheme.secondary, interactionSource = interactionSource, padding = 8.dp, interactionsConfig = cornerRadius = 28.dp, 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, ), ), verticalAlignment = Alignment.CenterVertically, ) { ) { Row(verticalAlignment = Alignment.CenterVertically) { Text( Text( "Keyboard Settings", "Keyboard Settings", color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant, Loading
packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt +137 −15 Original line number Original line Diff line number Diff line Loading @@ -17,10 +17,16 @@ package com.android.systemui.keyboard.shortcut.ui.composable package com.android.systemui.keyboard.shortcut.ui.composable import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.IndicationNodeFactory import androidx.compose.foundation.background import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.border import androidx.compose.foundation.clickable 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.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme Loading @@ -35,17 +41,27 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.remember import androidx.compose.ui.Modifier 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.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape 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.graphics.graphicsLayer import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import com.android.compose.modifiers.thenIf import com.android.compose.modifiers.thenIf import kotlinx.coroutines.launch /** /** * A selectable surface with no default focus/hover indications. * A selectable surface with no default focus/hover indications. Loading @@ -67,15 +83,17 @@ fun SelectableShortcutSurface( shadowElevation: Dp = 0.dp, shadowElevation: Dp = 0.dp, border: BorderStroke? = null, border: BorderStroke? = null, interactionSource: MutableInteractionSource? = null, interactionSource: MutableInteractionSource? = null, content: @Composable () -> Unit interactionsConfig: InteractionsConfig = InteractionsConfig(), content: @Composable () -> Unit, ) { ) { @Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation CompositionLocalProvider( CompositionLocalProvider( LocalContentColor provides contentColor, LocalContentColor provides contentColor, LocalAbsoluteTonalElevation provides absoluteElevation LocalAbsoluteTonalElevation provides absoluteElevation, ) { ) { val isFocused = interactionSource.collectIsFocusedAsState() Box( Box( modifier = modifier = modifier modifier Loading @@ -85,16 +103,18 @@ fun SelectableShortcutSurface( backgroundColor = backgroundColor = surfaceColorAtElevation(color = color, elevation = absoluteElevation), surfaceColorAtElevation(color = color, elevation = absoluteElevation), border = border, border = border, shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() } shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }, ) ) .selectable( .selectable( selected = selected, selected = selected, interactionSource = interactionSource, interactionSource = interactionSource, indication = null, indication = ShortcutHelperIndication(interactionSource, interactionsConfig), enabled = enabled, enabled = enabled, onClick = onClick onClick = onClick, ), ) propagateMinConstraints = true .thenIf(isFocused.value) { Modifier.zIndex(1f) }, propagateMinConstraints = true, ) { ) { content() content() } } Loading @@ -120,14 +140,15 @@ fun ClickableShortcutSurface( shadowElevation: Dp = 0.dp, shadowElevation: Dp = 0.dp, border: BorderStroke? = null, border: BorderStroke? = null, interactionSource: MutableInteractionSource? = null, interactionSource: MutableInteractionSource? = null, content: @Composable () -> Unit interactionsConfig: InteractionsConfig = InteractionsConfig(), content: @Composable () -> Unit, ) { ) { @Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation CompositionLocalProvider( CompositionLocalProvider( LocalContentColor provides contentColor, LocalContentColor provides contentColor, LocalAbsoluteTonalElevation provides absoluteElevation LocalAbsoluteTonalElevation provides absoluteElevation, ) { ) { Box( Box( modifier = modifier = Loading @@ -138,15 +159,16 @@ fun ClickableShortcutSurface( backgroundColor = backgroundColor = surfaceColorAtElevation(color = color, elevation = absoluteElevation), surfaceColorAtElevation(color = color, elevation = absoluteElevation), border = border, border = border, shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() } shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }, ) ) .clickable( .clickable( interactionSource = interactionSource, interactionSource = interactionSource, indication = null, indication = ShortcutHelperIndication(interactionSource, interactionsConfig), enabled = enabled, enabled = enabled, onClick = onClick onClick = onClick, ), ), propagateMinConstraints = true propagateMinConstraints = true, ) { ) { content() content() } } Loading Loading @@ -195,5 +217,105 @@ private fun Modifier.surface( } } .thenIf(border != null) { Modifier.border(border!!, shape) } .thenIf(border != null) { Modifier.border(border!!, shape) } .background(color = backgroundColor, shape = 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, )