diff options
5 files changed, 218 insertions, 6 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 476cced6a03d..e329aaee9a06 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 @@ -181,9 +181,11 @@ import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.ui.viewmodel.ResizeInfo +import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel import com.android.systemui.communal.util.DensityUtils.Companion.adjustedDp import com.android.systemui.communal.widgets.SmartspaceAppWidgetHostView import com.android.systemui.communal.widgets.WidgetConfigurator +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialogFactory import kotlin.math.max @@ -665,6 +667,7 @@ private fun ResizableItemFrameWrapper( maxHeightPx: Int, modifier: Modifier = Modifier, alpha: () -> Float = { 1f }, + viewModel: ResizeableItemFrameViewModel, onResize: (info: ResizeInfo) -> Unit = {}, content: @Composable (modifier: Modifier) -> Unit, ) { @@ -680,6 +683,7 @@ private fun ResizableItemFrameWrapper( enabled = enabled, alpha = alpha, modifier = modifier, + viewModel = viewModel, onResize = onResize, minHeightPx = minHeightPx, maxHeightPx = maxHeightPx, @@ -796,6 +800,14 @@ private fun BoxScope.CommunalHubLazyGrid( false } + val resizeableItemFrameViewModel = + rememberViewModel( + key = item.size.span, + traceName = "ResizeableItemFrame.viewModel.$index", + ) { + ResizeableItemFrameViewModel() + } + if (viewModel.isEditMode && dragDropState != null) { val isItemDragging = dragDropState.draggingItemKey == item.key val outlineAlpha by @@ -821,6 +833,7 @@ private fun BoxScope.CommunalHubLazyGrid( ) } .thenIf(isItemDragging) { Modifier.zIndex(1f) }, + viewModel = resizeableItemFrameViewModel, onResize = { resizeInfo -> contentListState.resize(index, resizeInfo) }, minHeightPx = widgetSizeInfo.minHeightPx, maxHeightPx = widgetSizeInfo.maxHeightPx, @@ -843,6 +856,7 @@ private fun BoxScope.CommunalHubLazyGrid( contentListState = contentListState, interactionHandler = interactionHandler, widgetSection = widgetSection, + resizeableItemFrameViewModel = resizeableItemFrameViewModel, ) } } @@ -857,6 +871,7 @@ private fun BoxScope.CommunalHubLazyGrid( contentListState = contentListState, interactionHandler = interactionHandler, widgetSection = widgetSection, + resizeableItemFrameViewModel = resizeableItemFrameViewModel, ) } } @@ -1080,6 +1095,7 @@ private fun CommunalContent( contentListState: ContentListState, interactionHandler: RemoteViews.InteractionHandler?, widgetSection: CommunalAppWidgetSection, + resizeableItemFrameViewModel: ResizeableItemFrameViewModel, ) { when (model) { is CommunalContentModel.WidgetContent.Widget -> @@ -1093,6 +1109,7 @@ private fun CommunalContent( index, contentListState, widgetSection, + resizeableItemFrameViewModel, ) is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier) is CommunalContentModel.WidgetContent.DisabledWidget -> @@ -1223,7 +1240,9 @@ private fun WidgetContent( index: Int, contentListState: ContentListState, widgetSection: CommunalAppWidgetSection, + resizeableItemFrameViewModel: ResizeableItemFrameViewModel, ) { + val coroutineScope = rememberCoroutineScope() val context = LocalContext.current val accessibilityLabel = remember(model, context) { @@ -1234,6 +1253,10 @@ private fun WidgetContent( val placeWidgetActionLabel = stringResource(R.string.accessibility_action_label_place_widget) val unselectWidgetActionLabel = stringResource(R.string.accessibility_action_label_unselect_widget) + + val shrinkWidgetLabel = stringResource(R.string.accessibility_action_label_shrink_widget) + val expandWidgetLabel = stringResource(R.string.accessibility_action_label_expand_widget) + val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle() val selectedIndex = selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } } @@ -1292,6 +1315,29 @@ private fun WidgetContent( true } val actions = mutableListOf(deleteAction) + + if (communalWidgetResizing() && resizeableItemFrameViewModel.canShrink()) { + actions.add( + CustomAccessibilityAction(shrinkWidgetLabel) { + coroutineScope.launch { + resizeableItemFrameViewModel.shrinkToNextAnchor() + } + true + } + ) + } + + if (communalWidgetResizing() && resizeableItemFrameViewModel.canExpand()) { + actions.add( + CustomAccessibilityAction(expandWidgetLabel) { + coroutineScope.launch { + resizeableItemFrameViewModel.expandToNextAnchor() + } + true + } + ) + } + if (selectedIndex != null && selectedIndex != index) { actions.add( CustomAccessibilityAction(placeWidgetActionLabel) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt index 521330f60fa8..8e85432f4f36 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt @@ -56,7 +56,6 @@ import com.android.compose.modifiers.thenIf import com.android.systemui.communal.ui.viewmodel.DragHandle import com.android.systemui.communal.ui.viewmodel.ResizeInfo import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel -import com.android.systemui.lifecycle.rememberViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -192,16 +191,12 @@ fun ResizableItemFrame( maxHeightPx: Int = Int.MAX_VALUE, resizeMultiple: Int = 1, alpha: () -> Float = { 1f }, + viewModel: ResizeableItemFrameViewModel, onResize: (info: ResizeInfo) -> Unit = {}, content: @Composable () -> Unit, ) { val brush = SolidColor(outlineColor) val onResizeUpdated by rememberUpdatedState(onResize) - val viewModel = - rememberViewModel(key = currentSpan, traceName = "ResizeableItemFrame.viewModel") { - ResizeableItemFrameViewModel() - } - val dragHandleHeight = verticalArrangement.spacing - outlinePadding * 2 val isDragging by remember(viewModel) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt index 22b114c632cd..0269577af789 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt @@ -366,6 +366,106 @@ class ResizeableItemFrameViewModelTest : SysuiTestCase() { assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f) } + @Test + fun testCanExpand_atTopPosition_withMultipleAnchors_returnsTrue() = + testScope.runTest { + val twoRowGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 0) + + updateGridLayout(twoRowGrid) + assertThat(underTest.canExpand()).isTrue() + assertThat(underTest.bottomDragState.anchors.toList()) + .containsAtLeast(0 to 0f, 1 to 45f) + } + + @Test + fun testCanExpand_atTopPosition_withSingleAnchors_returnsFalse() = + testScope.runTest { + val oneRowGrid = singleSpanGrid.copy(totalSpans = 1, currentSpan = 1, currentRow = 0) + updateGridLayout(oneRowGrid) + assertThat(underTest.canExpand()).isFalse() + } + + @Test + fun testCanExpand_atBottomPosition_withMultipleAnchors_returnsTrue() = + testScope.runTest { + val twoRowGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 1) + updateGridLayout(twoRowGrid) + assertThat(underTest.canExpand()).isTrue() + assertThat(underTest.topDragState.anchors.toList()).containsAtLeast(0 to 0f, -1 to -45f) + } + + @Test + fun testCanShrink_atMinimumHeight_returnsFalse() = + testScope.runTest { + val oneRowGrid = singleSpanGrid.copy(totalSpans = 1, currentSpan = 1, currentRow = 0) + updateGridLayout(oneRowGrid) + assertThat(underTest.canShrink()).isFalse() + } + + @Test + fun testCanShrink_atFullSize_checksBottomDragState() = runTestWithSnapshots { + val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 2, currentRow = 0) + updateGridLayout(twoSpanGrid) + + assertThat(underTest.canShrink()).isTrue() + assertThat(underTest.bottomDragState.anchors.toList()).containsAtLeast(0 to 0f, -1 to -45f) + } + + @Test + fun testResizeByAccessibility_expandFromBottom_usesTopDragState() = runTestWithSnapshots { + val resizeInfo by collectLastValue(underTest.resizeInfo) + + val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 1) + updateGridLayout(twoSpanGrid) + + underTest.expandToNextAnchor() + + assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.TOP)) + } + + @Test + fun testResizeByAccessibility_expandFromTop_usesBottomDragState() = runTestWithSnapshots { + val resizeInfo by collectLastValue(underTest.resizeInfo) + + val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 0) + updateGridLayout(twoSpanGrid) + + underTest.expandToNextAnchor() + + assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.BOTTOM)) + } + + @Test + fun testResizeByAccessibility_shrinkFromFull_usesBottomDragState() = runTestWithSnapshots { + val resizeInfo by collectLastValue(underTest.resizeInfo) + + val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 2, currentRow = 0) + updateGridLayout(twoSpanGrid) + + underTest.shrinkToNextAnchor() + + assertThat(resizeInfo).isEqualTo(ResizeInfo(-1, DragHandle.BOTTOM)) + } + + @Test + fun testResizeByAccessibility_cannotResizeAtMinSize() = runTestWithSnapshots { + val resizeInfo by collectLastValue(underTest.resizeInfo) + + // Set up grid at minimum size + val minSizeGrid = + singleSpanGrid.copy( + totalSpans = 2, + currentSpan = 1, + minHeightPx = singleSpanGrid.minHeightPx, + currentRow = 0, + ) + updateGridLayout(minSizeGrid) + + underTest.shrinkToNextAnchor() + + assertThat(resizeInfo).isNull() + } + @Test(expected = IllegalArgumentException::class) fun testIllegalState_maxHeightLessThanMinHeight() = testScope.runTest { diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 0aa5ccf7a2b4..c838180f9541 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1311,6 +1311,10 @@ <string name="communal_widget_picker_description">Anyone can view widgets on your lock screen, even if your tablet\'s locked.</string> <!-- Label for accessibility action to unselect a widget in edit mode. [CHAR LIMIT=NONE] --> <string name="accessibility_action_label_unselect_widget">unselect widget</string> + <!-- Label for accessibility action to shrink a widget in edit mode. [CHAR LIMIT=NONE] --> + <string name="accessibility_action_label_shrink_widget">Decrease height</string> + <!-- Label for accessibility action to expand a widget in edit mode. [CHAR LIMIT=NONE] --> + <string name="accessibility_action_label_expand_widget">Increase height</string> <!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] --> <string name="communal_widgets_disclaimer_title">Lock screen widgets</string> <!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt index db4bee781a58..bde5d0f87a66 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.communal.ui.viewmodel import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.snapTo import androidx.compose.runtime.snapshotFlow import com.android.app.tracing.coroutines.coroutineScopeTraced as coroutineScope import com.android.systemui.lifecycle.ExclusiveActivatable @@ -81,6 +82,72 @@ class ResizeableItemFrameViewModel : ExclusiveActivatable() { get() = roundDownToMultiple(getSpansForPx(minHeightPx)) } + /** Check if widget can expanded based on current drag states */ + fun canExpand(): Boolean { + return getNextAnchor(bottomDragState, moveUp = false) != null || + getNextAnchor(topDragState, moveUp = true) != null + } + + /** Check if widget can shrink based on current drag states */ + fun canShrink(): Boolean { + return getNextAnchor(bottomDragState, moveUp = true) != null || + getNextAnchor(topDragState, moveUp = false) != null + } + + /** Get the next anchor value in the specified direction */ + private fun getNextAnchor(state: AnchoredDraggableState<Int>, moveUp: Boolean): Int? { + var nextAnchor: Int? = null + var nextAnchorDiff = Int.MAX_VALUE + val currentValue = state.currentValue + + for (i in 0 until state.anchors.size) { + val anchor = state.anchors.anchorAt(i) ?: continue + if (anchor == currentValue) continue + + val diff = + if (moveUp) { + currentValue - anchor + } else { + anchor - currentValue + } + + if (diff in 1..<nextAnchorDiff) { + nextAnchor = anchor + nextAnchorDiff = diff + } + } + + return nextAnchor + } + + /** Handle expansion to the next anchor */ + suspend fun expandToNextAnchor() { + if (!canExpand()) return + val bottomAnchor = getNextAnchor(state = bottomDragState, moveUp = false) + if (bottomAnchor != null) { + bottomDragState.snapTo(bottomAnchor) + return + } + val topAnchor = + getNextAnchor( + state = topDragState, + moveUp = true, // Moving up to expand + ) + topAnchor?.let { topDragState.snapTo(it) } + } + + /** Handle shrinking to the next anchor */ + suspend fun shrinkToNextAnchor() { + if (!canShrink()) return + val topAnchor = getNextAnchor(state = topDragState, moveUp = false) + if (topAnchor != null) { + topDragState.snapTo(topAnchor) + return + } + val bottomAnchor = getNextAnchor(state = bottomDragState, moveUp = true) + bottomAnchor?.let { bottomDragState.snapTo(it) } + } + /** * The layout information necessary in order to calculate the pixel offsets of the drag anchor * points. |