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

Commit 116b2264 authored by Lucas Silva's avatar Lucas Silva Committed by Android (Google) Code Review
Browse files

Merge "Make items selectable in the communal hub" into main

parents 781fe4fe b215ead7
Loading
Loading
Loading
Loading
+101 −30
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.appwidget.AppWidgetHostView
import android.os.Bundle
import android.util.SizeF
import android.widget.FrameLayout
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@@ -38,6 +39,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -58,17 +60,22 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
@@ -86,6 +93,9 @@ import androidx.compose.ui.window.Popup
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.ui.compose.extensions.allowGestures
import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
import com.android.systemui.communal.ui.compose.extensions.observeTapsWithoutConsuming
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.res.R
@@ -104,22 +114,59 @@ fun CommunalHub(
    var toolbarSize: IntSize? by remember { mutableStateOf(null) }
    var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
    var isDraggingToRemove by remember { mutableStateOf(false) }
    val gridState = rememberLazyGridState()
    val contentListState = rememberContentListState(communalContent, viewModel)
    val reorderingWidgets by viewModel.reorderingWidgets.collectAsState()
    val selectedIndex = viewModel.selectedIndex.collectAsState()
    val removeButtonEnabled by remember {
        derivedStateOf { selectedIndex.value != null || reorderingWidgets }
    }

    val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize)
    val contentOffset = beforeContentPadding(contentPadding).toOffset()

    Box(
        modifier =
            modifier.fillMaxSize().background(LocalAndroidColorScheme.current.outlineVariant),
            modifier
                .fillMaxSize()
                .background(LocalAndroidColorScheme.current.outlineVariant)
                .pointerInput(gridState, contentOffset, contentListState) {
                    // If not in edit mode, don't allow selecting items.
                    if (!viewModel.isEditMode) return@pointerInput
                    observeTapsWithoutConsuming { offset ->
                        val adjustedOffset = offset - contentOffset
                        val index =
                            gridState.layoutInfo.visibleItemsInfo
                                .firstItemAtOffset(adjustedOffset)
                                ?.index
                        val newIndex =
                            if (index?.let(contentListState::isItemEditable) == true) {
                                index
                            } else {
                                null
                            }
                        viewModel.setSelectedIndex(newIndex)
                    }
                },
    ) {
        CommunalHubLazyGrid(
            communalContent = communalContent,
            viewModel = viewModel,
            contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize),
            contentPadding = contentPadding,
            contentOffset = contentOffset,
            setGridCoordinates = { gridCoordinates = it },
            updateDragPositionForRemove = {
            updateDragPositionForRemove = { offset ->
                isDraggingToRemove =
                    checkForDraggingToRemove(it, removeButtonCoordinates, gridCoordinates)
                    isPointerWithinCoordinates(
                        offset = gridCoordinates?.let { it.positionInWindow() + offset },
                        containerToCheck = removeButtonCoordinates
                    )
                isDraggingToRemove
            },
            onOpenWidgetPicker = onOpenWidgetPicker,
            gridState = gridState,
            contentListState = contentListState,
            selectedIndex = selectedIndex
        )

        if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) {
@@ -129,6 +176,14 @@ fun CommunalHub(
                setRemoveButtonCoordinates = { removeButtonCoordinates = it },
                onEditDone = onEditDone,
                onOpenWidgetPicker = onOpenWidgetPicker,
                onRemoveClicked = {
                    selectedIndex.value?.let { index ->
                        contentListState.onRemove(index)
                        contentListState.onSaveList()
                        viewModel.setSelectedIndex(null)
                    }
                },
                removeEnabled = removeButtonEnabled
            )
        } else {
            IconButton(onClick = viewModel::onOpenWidgetEditor) {
@@ -158,16 +213,18 @@ private fun BoxScope.CommunalHubLazyGrid(
    communalContent: List<CommunalContentModel>,
    viewModel: BaseCommunalViewModel,
    contentPadding: PaddingValues,
    selectedIndex: State<Int?>,
    contentOffset: Offset,
    gridState: LazyGridState,
    contentListState: ContentListState,
    setGridCoordinates: (coordinates: LayoutCoordinates) -> Unit,
    updateDragPositionForRemove: (offset: Offset) -> Boolean,
    onOpenWidgetPicker: (() -> Unit)? = null,
) {
    var gridModifier = Modifier.align(Alignment.CenterStart)
    val gridState = rememberLazyGridState()
    var list = communalContent
    var dragDropState: GridDragDropState? = null
    if (viewModel.isEditMode && viewModel is CommunalEditModeViewModel) {
        val contentListState = rememberContentListState(list, viewModel)
        list = contentListState.list
        // for drag & drop operations within the communal hub grid
        dragDropState =
@@ -179,7 +236,7 @@ private fun BoxScope.CommunalHubLazyGrid(
        gridModifier =
            gridModifier
                .fillMaxSize()
                .dragContainer(dragDropState, beforeContentPadding(contentPadding), viewModel)
                .dragContainer(dragDropState, contentOffset, viewModel)
                .onGloballyPositioned { setGridCoordinates(it) }
        // for widgets dropped from other activities
        val dragAndDropTargetState =
@@ -218,8 +275,10 @@ private fun BoxScope.CommunalHubLazyGrid(
                    list[index].size.dp().value,
                )
            if (viewModel.isEditMode && dragDropState != null) {
                val selected by remember(index) { derivedStateOf { index == selectedIndex.value } }
                DraggableItem(
                    dragDropState = dragDropState,
                    selected = selected,
                    enabled = list[index] is CommunalContentModel.Widget,
                    index = index,
                    size = size
@@ -253,11 +312,19 @@ private fun BoxScope.CommunalHubLazyGrid(
@Composable
private fun Toolbar(
    isDraggingToRemove: Boolean,
    removeEnabled: Boolean,
    onRemoveClicked: () -> Unit,
    setToolbarSize: (toolbarSize: IntSize) -> Unit,
    setRemoveButtonCoordinates: (coordinates: LayoutCoordinates) -> Unit,
    onOpenWidgetPicker: () -> Unit,
    onEditDone: () -> Unit,
    onEditDone: () -> Unit
) {
    val removeButtonAlpha: Float by
        animateFloatAsState(
            targetValue = if (removeEnabled) 1f else 0.5f,
            label = "RemoveButtonAlphaAnimation"
        )

    Row(
        modifier =
            Modifier.fillMaxWidth()
@@ -301,13 +368,18 @@ private fun Toolbar(
            }
        } else {
            OutlinedButton(
                // Button is disabled to make it non-clickable
                enabled = false,
                onClick = {},
                colors = ButtonDefaults.outlinedButtonColors(disabledContentColor = colors.primary),
                enabled = removeEnabled,
                onClick = onRemoveClicked,
                colors =
                    ButtonDefaults.outlinedButtonColors(
                        contentColor = colors.primary,
                        disabledContentColor = colors.primary
                    ),
                border = BorderStroke(width = 1.0.dp, color = colors.primary),
                contentPadding = Dimensions.ButtonPadding,
                modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) }
                modifier =
                    Modifier.graphicsLayer { alpha = removeButtonAlpha }
                        .onGloballyPositioned { setRemoveButtonCoordinates(it) }
            ) {
                RemoveButtonContent(spacerModifier)
            }
@@ -385,7 +457,7 @@ private fun CommunalContent(
) {
    when (model) {
        is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, modifier)
        is CommunalContentModel.WidgetPlaceholder -> WidgetPlaceholderContent(size)
        is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(size)
        is CommunalContentModel.CtaTileInViewMode ->
            CtaTileInViewModeContent(viewModel, size, modifier)
        is CommunalContentModel.CtaTileInEditMode ->
@@ -396,11 +468,11 @@ private fun CommunalContent(
    }
}

/** Presents a placeholder card for the new widget being dragged and dropping into the grid. */
/** Creates an empty card used to highlight a particular spot on the grid. */
@Composable
fun WidgetPlaceholderContent(size: SizeF) {
fun HighlightedItem(size: SizeF, modifier: Modifier = Modifier) {
    Card(
        modifier = Modifier.size(Dp(size.width), Dp(size.height)),
        modifier = modifier.size(Dp(size.width), Dp(size.height)),
        colors = CardDefaults.cardColors(containerColor = Color.Transparent),
        border = BorderStroke(3.dp, LocalAndroidColorScheme.current.tertiaryFixed),
        shape = RoundedCornerShape(16.dp)
@@ -528,7 +600,7 @@ private fun WidgetContent(
        contentAlignment = Alignment.Center,
    ) {
        AndroidView(
            modifier = modifier,
            modifier = modifier.allowGestures(allowed = !viewModel.isEditMode),
            factory = { context ->
                // The AppWidgetHostView will inherit the interaction handler from the
                // AppWidgetHost. So set the interaction handler here before creating the view, and
@@ -616,8 +688,8 @@ private fun gridContentPadding(isEditMode: Boolean, toolbarSize: IntSize?): Padd
private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingInPx {
    return with(LocalDensity.current) {
        ContentPaddingInPx(
            startPadding = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(),
            topPadding = paddingValues.calculateTopPadding().toPx()
            start = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(),
            top = paddingValues.calculateTopPadding().toPx()
        )
    }
}
@@ -626,18 +698,15 @@ private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingIn
 * Check whether the pointer position that the item is being dragged at is within the coordinates of
 * the remove button in the toolbar. Returns true if the item is removable.
 */
private fun checkForDraggingToRemove(
    offset: Offset,
    removeButtonCoordinates: LayoutCoordinates?,
    gridCoordinates: LayoutCoordinates?,
private fun isPointerWithinCoordinates(
    offset: Offset?,
    containerToCheck: LayoutCoordinates?
): Boolean {
    if (removeButtonCoordinates == null || gridCoordinates == null) {
    if (offset == null || containerToCheck == null) {
        return false
    }
    val pointer = gridCoordinates.positionInWindow() + offset
    val removeButton = removeButtonCoordinates.positionInWindow()
    return pointer.x in removeButton.x..removeButton.x + removeButtonCoordinates.size.width &&
        pointer.y in removeButton.y..removeButton.y + removeButtonCoordinates.size.height
    val container = containerToCheck.boundsInWindow()
    return container.contains(offset)
}

private fun CommunalContentSize.dp(): Dp {
@@ -648,7 +717,9 @@ private fun CommunalContentSize.dp(): Dp {
    }
}

data class ContentPaddingInPx(val startPadding: Float, val topPadding: Float)
data class ContentPaddingInPx(val start: Float, val top: Float) {
    fun toOffset(): Offset = Offset(start, top)
}

object Dimensions {
    val CardWidth = 464.dp
+2 −2
Original line number Diff line number Diff line
@@ -21,12 +21,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel

@Composable
fun rememberContentListState(
    communalContent: List<CommunalContentModel>,
    viewModel: CommunalEditModeViewModel,
    viewModel: BaseCommunalViewModel,
): ContentListState {
    return remember(communalContent) {
        ContentListState(
+52 −40
Original line number Diff line number Diff line
@@ -17,6 +17,10 @@
package com.android.systemui.communal.ui.compose

import android.util.SizeF
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.scrollBy
@@ -32,6 +36,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
@@ -39,6 +44,7 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.zIndex
import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
import com.android.systemui.communal.ui.compose.extensions.plus
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import kotlinx.coroutines.CoroutineScope
@@ -109,13 +115,10 @@ internal constructor(

    internal fun onDragStart(offset: Offset, contentOffset: Offset) {
        state.layoutInfo.visibleItemsInfo
            .firstOrNull { item ->
            .filter { item -> contentListState.isItemEditable(item.index) }
            // grid item offset is based off grid content container so we need to deduct
            // before content padding from the initial pointer position
                contentListState.isItemEditable(item.index) &&
                    (offset.x - contentOffset.x).toInt() in item.offset.x..item.offsetEnd.x &&
                    (offset.y - contentOffset.y).toInt() in item.offset.y..item.offsetEnd.y
            }
            .firstItemAtOffset(offset - contentOffset)
            ?.apply {
                dragStartPointerOffset = offset - this.offset.toOffset()
                draggingItemIndex = index
@@ -148,12 +151,11 @@ internal constructor(
        val middleOffset = startOffset + (endOffset - startOffset) / 2f

        val targetItem =
            state.layoutInfo.visibleItemsInfo.find { item ->
                contentListState.isItemEditable(item.index) &&
                    middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x &&
                    middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y &&
                    draggingItem.index != item.index
            }
            state.layoutInfo.visibleItemsInfo
                .asSequence()
                .filter { item -> contentListState.isItemEditable(item.index) }
                .filter { item -> draggingItem.index != item.index }
                .firstItemAtOffset(middleOffset)

        if (targetItem != null) {
            val scrollToIndex =
@@ -208,20 +210,18 @@ internal constructor(

fun Modifier.dragContainer(
    dragDropState: GridDragDropState,
    beforeContentPadding: ContentPaddingInPx,
    contentOffset: Offset,
    viewModel: BaseCommunalViewModel,
): Modifier {
    return pointerInput(dragDropState, beforeContentPadding) {
    return this.then(
        pointerInput(dragDropState, contentOffset) {
            detectDragGesturesAfterLongPress(
                onDrag = { change, offset ->
                    change.consume()
                    dragDropState.onDrag(offset = offset)
                },
                onDragStart = { offset ->
                dragDropState.onDragStart(
                    offset,
                    Offset(beforeContentPadding.startPadding, beforeContentPadding.topPadding)
                )
                    dragDropState.onDragStart(offset, contentOffset)
                    viewModel.onReorderWidgetStart()
                },
                onDragEnd = {
@@ -234,6 +234,7 @@ fun Modifier.dragContainer(
                }
            )
        }
    )
}

/** Wrap LazyGrid item with additional modifier needed for drag and drop. */
@@ -243,6 +244,7 @@ fun LazyGridItemScope.DraggableItem(
    dragDropState: GridDragDropState,
    index: Int,
    enabled: Boolean,
    selected: Boolean,
    size: SizeF,
    modifier: Modifier = Modifier,
    content: @Composable (isDragging: Boolean) -> Unit
@@ -250,21 +252,31 @@ fun LazyGridItemScope.DraggableItem(
    if (!enabled) {
        return Box(modifier = modifier) { content(false) }
    }

    val dragging = index == dragDropState.draggingItemIndex
    val itemAlpha: Float by
        animateFloatAsState(
            targetValue = if (dragDropState.isDraggingToRemove) 0.5f else 1f,
            label = "DraggableItemAlpha"
        )
    val draggingModifier =
        if (dragging) {
            Modifier.zIndex(1f).graphicsLayer {
                translationX = dragDropState.draggingItemOffset.x
                translationY = dragDropState.draggingItemOffset.y
                alpha = if (dragDropState.isDraggingToRemove) 0.5f else 1f
                alpha = itemAlpha
            }
        } else {
            Modifier.animateItemPlacement()
        }

    Box(modifier) {
        if (dragging) {
            WidgetPlaceholderContent(size)
        AnimatedVisibility(
            visible = (dragging || selected) && !dragDropState.isDraggingToRemove,
            enter = fadeIn(),
            exit = fadeOut()
        ) {
            HighlightedItem(size)
        }
        Box(modifier = draggingModifier, propagateMinConstraints = true) { content(dragging) }
    }
+47 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 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
 *
 *      http://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.android.systemui.communal.ui.compose.extensions

import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.toRect

/**
 * Determine the item at the specified offset, or null if none exist.
 *
 * @param offset The offset in pixels, relative to the top start of the grid.
 */
fun Iterable<LazyGridItemInfo>.firstItemAtOffset(offset: Offset): LazyGridItemInfo? =
    firstOrNull { item ->
        isItemAtOffset(item, offset)
    }

/**
 * Determine the item at the specified offset, or null if none exist.
 *
 * @param offset The offset in pixels, relative to the top start of the grid.
 */
fun Sequence<LazyGridItemInfo>.firstItemAtOffset(offset: Offset): LazyGridItemInfo? =
    firstOrNull { item ->
        isItemAtOffset(item, offset)
    }

private fun isItemAtOffset(item: LazyGridItemInfo, offset: Offset): Boolean {
    val boundingBox = IntRect(item.offset, item.size)
    return boundingBox.toRect().contains(offset)
}
+28 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 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
 *
 *      http://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.android.systemui.communal.ui.compose.extensions

import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput

/** Sets whether gestures are allowed on children of this element. */
fun Modifier.allowGestures(allowed: Boolean): Modifier =
    if (allowed) {
        this
    } else {
        this.then(pointerInput(Unit) { consumeAllGestures() })
    }
Loading