diff options
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) } } |