Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 579da54f authored by Josh's avatar Josh
Browse files

Implemented Interaction states for Shortcut Helper

- added hover/pressed state implementations for categories and keyboard settings
- Refactored Shortcut helper surfaces to have hover/focused/pressed
  state implementations by default allowing for reuse.

Test: Manual - ensure that shortcut helper categories UI and keyboard
settings respond to hover/press/focus as shown in UI Mocks
Flag: com.android.systemui.keyboard_shortcut_helper_rewrite
Fixes: 355154005

Change-Id: Id33c15dfe6d1a63e282025d4c7286c9bf333be4d
parent e6f643bb
Loading
Loading
Loading
Loading
+33 −29
Original line number Diff line number Diff line
@@ -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
    ) {
        Row(
        modifier =
                Modifier.padding(horizontal = 12.dp, vertical = 16.dp)
                    .outlineFocusModifier(
                        isFocused = isFocused,
                        focusColor = MaterialTheme.colorScheme.secondary,
                        padding = 8.dp,
                        cornerRadius = 28.dp,
            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,
            ),
            verticalAlignment = Alignment.CenterVertically,
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Text(
                "Keyboard Settings",
                color = MaterialTheme.colorScheme.onSurfaceVariant,
+137 −15
Original line number Diff line number Diff line
@@ -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,
)