diff options
author | 2024-01-15 23:59:32 -0500 | |
---|---|---|
committer | 2024-01-16 15:39:53 -0500 | |
commit | b215ead717da792d78be48ac6b9e67fcb786ff6d (patch) | |
tree | 2f6cdc1b25d76580763b2186853e9652afc5a19e | |
parent | 113e9181211363b366331ca7c2e39a94957472d3 (diff) |
Make items selectable in the communal hub
This change makes items in the hub selectable by tapping on them. Once
an item is selected, it can also be removed by tapping on the remove
button. Tapping elsewhere on the screen will unselect the item or select
another one.
Test: flashed and verified changes on-device by selecting/unselecting
items
Flag: ACONFIG com.android.systemui.communal_hub DEVELOPMENT
Bug: 318537189
Change-Id: I62bef163e1b94d26b7fe1b5aa60ce619351dade6
8 files changed, 316 insertions, 75 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index d76f0ff3ec18..f8db596cd042 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -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.media.controls.ui.MediaHierarchyManager @@ -106,22 +116,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) { @@ -131,6 +178,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) { @@ -160,16 +215,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 = @@ -181,7 +238,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 = @@ -220,8 +277,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 @@ -255,11 +314,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() @@ -303,13 +370,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) } @@ -387,7 +459,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 -> @@ -398,11 +470,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) @@ -530,7 +602,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 @@ -622,8 +694,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() ) } } @@ -632,18 +704,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 { @@ -654,7 +723,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 diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt index 979991d7dc2a..45f98b879dd7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt @@ -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( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt index 113822167ca7..a1959532fbb9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt @@ -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 -> - // 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 - } + .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 + .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,32 +210,31 @@ internal constructor( fun Modifier.dragContainer( dragDropState: GridDragDropState, - beforeContentPadding: ContentPaddingInPx, + contentOffset: Offset, viewModel: BaseCommunalViewModel, ): Modifier { - return pointerInput(dragDropState, beforeContentPadding) { - detectDragGesturesAfterLongPress( - onDrag = { change, offset -> - change.consume() - dragDropState.onDrag(offset = offset) - }, - onDragStart = { offset -> - dragDropState.onDragStart( - offset, - Offset(beforeContentPadding.startPadding, beforeContentPadding.topPadding) - ) - viewModel.onReorderWidgetStart() - }, - onDragEnd = { - dragDropState.onDragInterrupted() - viewModel.onReorderWidgetEnd() - }, - onDragCancel = { - dragDropState.onDragInterrupted() - viewModel.onReorderWidgetCancel() - } - ) - } + return this.then( + pointerInput(dragDropState, contentOffset) { + detectDragGesturesAfterLongPress( + onDrag = { change, offset -> + change.consume() + dragDropState.onDrag(offset = offset) + }, + onDragStart = { offset -> + dragDropState.onDragStart(offset, contentOffset) + viewModel.onReorderWidgetStart() + }, + onDragEnd = { + dragDropState.onDragInterrupted() + viewModel.onReorderWidgetEnd() + }, + onDragCancel = { + dragDropState.onDragInterrupted() + viewModel.onReorderWidgetCancel() + } + ) + } + ) } /** 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) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt new file mode 100644 index 000000000000..132093f034bb --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt @@ -0,0 +1,47 @@ +/* + * 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) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt new file mode 100644 index 000000000000..b31008e04593 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt @@ -0,0 +1,28 @@ +/* + * 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() }) + } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt new file mode 100644 index 000000000000..14074944259b --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt @@ -0,0 +1,54 @@ +/* + * 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.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import kotlinx.coroutines.coroutineScope + +/** + * Observe taps without actually consuming them, so child elements can still respond to them. Long + * presses are excluded. + */ +suspend fun PointerInputScope.observeTapsWithoutConsuming( + pass: PointerEventPass = PointerEventPass.Initial, + onTap: ((Offset) -> Unit)? = null, +) = coroutineScope { + if (onTap == null) return@coroutineScope + awaitEachGesture { + awaitFirstDown(pass = pass) + val tapTimeout = viewConfiguration.longPressTimeoutMillis + val up = withTimeoutOrNull(tapTimeout) { waitForUpOrCancellation(pass = pass) } + if (up != null) { + onTap(up.position) + } + } +} + +/** Consume all gestures on the initial pass so that child elements do not receive them. */ +suspend fun PointerInputScope.consumeAllGestures() = coroutineScope { + awaitEachGesture { + awaitPointerEvent(pass = PointerEventPass.Initial) + .changes + .forEach(PointerInputChange::consume) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 84708a49f469..4da348e6a92a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -24,6 +24,7 @@ import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.media.controls.ui.MediaHost import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOf @@ -36,6 +37,15 @@ abstract class BaseCommunalViewModel( val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene + /** Whether widgets are currently being re-ordered. */ + open val reorderingWidgets: StateFlow<Boolean> = MutableStateFlow(false) + + private val _selectedIndex: MutableStateFlow<Int?> = MutableStateFlow(null) + + /** The index of the currently selected item, or null if no item selected. */ + val selectedIndex: StateFlow<Int?> + get() = _selectedIndex + fun onSceneChanged(scene: CommunalSceneKey) { communalInteractor.onSceneChanged(scene) } @@ -105,4 +115,9 @@ abstract class BaseCommunalViewModel( /** Called as the user cancels dragging a widget to reorder. */ open fun onReorderWidgetCancel() {} + + /** Set the index of the currently selected item */ + fun setSelectedIndex(index: Int?) { + _selectedIndex.value = index + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 7faf653cc177..fcad45f950dc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -37,7 +37,10 @@ import javax.inject.Named import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach /** The view model for communal hub in edit mode. */ @SysUISingleton @@ -69,9 +72,15 @@ constructor( // Only widgets are editable. The CTA tile comes last in the list and remains visible. override val communalContent: Flow<List<CommunalContentModel>> = - communalInteractor.widgetContent.map { widgets -> - widgets + listOf(CommunalContentModel.CtaTileInEditMode()) - } + communalInteractor.widgetContent + // Clear the selected index when the list is updated. + .onEach { setSelectedIndex(null) } + .map { widgets -> widgets + listOf(CommunalContentModel.CtaTileInEditMode()) } + + private val _reorderingWidgets = MutableStateFlow(false) + + override val reorderingWidgets: StateFlow<Boolean> + get() = _reorderingWidgets override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id) @@ -135,14 +144,19 @@ constructor( } override fun onReorderWidgetStart() { + // Clear selection status + setSelectedIndex(null) + _reorderingWidgets.value = true uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) } override fun onReorderWidgetEnd() { + _reorderingWidgets.value = false uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH) } override fun onReorderWidgetCancel() { + _reorderingWidgets.value = false uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } } |