Loading packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +47 −0 Original line number Diff line number Diff line Loading @@ -97,11 +97,13 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Popup import androidx.core.view.setPadding import com.android.compose.modifiers.thenIf 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.Dimensions.CardOutlineWidth import com.android.systemui.communal.ui.compose.extensions.allowGestures import com.android.systemui.communal.ui.compose.extensions.detectLongPressGesture 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 Loading Loading @@ -132,6 +134,8 @@ fun CommunalHub( val removeButtonEnabled by remember { derivedStateOf { selectedIndex.value != null || reorderingWidgets } } val (isButtonToEditWidgetsShowing, setIsButtonToEditWidgetsShowing) = remember { mutableStateOf(false) } val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize) val contentOffset = beforeContentPadding(contentPadding).toOffset() Loading @@ -158,6 +162,11 @@ fun CommunalHub( } viewModel.setSelectedIndex(newIndex) } } .thenIf(!viewModel.isEditMode) { Modifier.pointerInput(Unit) { detectLongPressGesture { offset -> setIsButtonToEditWidgetsShowing(true) } } }, ) { CommunalHubLazyGrid( Loading Loading @@ -207,6 +216,16 @@ fun CommunalHub( PopupOnDismissCtaTile(viewModel::onHidePopupAfterDismissCta) } if (isButtonToEditWidgetsShowing) { ButtonToEditWidgets( onClick = { setIsButtonToEditWidgetsShowing(false) viewModel.onOpenWidgetEditor() }, onHide = { setIsButtonToEditWidgetsShowing(false) }, ) } // This spacer covers the edge of the LazyHorizontalGrid and prevents it from receiving // touches, so that the SceneTransitionLayout can intercept the touches and allow an edge // swipe back to the blank scene. Loading Loading @@ -413,6 +432,34 @@ private fun Toolbar( } } @Composable private fun ButtonToEditWidgets( onClick: () -> Unit, onHide: () -> Unit, ) { Popup(alignment = Alignment.TopCenter, offset = IntOffset(0, 40), onDismissRequest = onHide) { val colors = LocalAndroidColorScheme.current Button( modifier = Modifier.height(56.dp).background(colors.secondary, RoundedCornerShape(50.dp)), onClick = onClick, ) { Icon( imageVector = Icons.Outlined.Widgets, contentDescription = stringResource(R.string.button_to_configure_widgets_text), tint = colors.onSecondary, modifier = Modifier.size(20.dp) ) Spacer(modifier = Modifier.size(8.dp)) Text( text = stringResource(R.string.button_to_configure_widgets_text), style = MaterialTheme.typography.titleSmall, color = colors.onSecondary, ) } } } @Composable private fun PopupOnDismissCtaTile(onHidePopupAfterDismissCta: () -> Unit) { Popup( Loading packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt +39 −0 Original line number Diff line number Diff line Loading @@ -20,9 +20,13 @@ 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.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach import kotlinx.coroutines.coroutineScope /** Loading @@ -44,6 +48,41 @@ suspend fun PointerInputScope.observeTapsWithoutConsuming( } } /** * Detect long press gesture and calls onLongPress when detected. The callback parameter receives an * Offset representing the position relative to the containing element. */ suspend fun PointerInputScope.detectLongPressGesture( pass: PointerEventPass = PointerEventPass.Initial, onLongPress: ((Offset) -> Unit), ) = coroutineScope { awaitEachGesture { val down = awaitFirstDown(pass = pass) val longPressTimeout = viewConfiguration.longPressTimeoutMillis // wait for first tap up or long press try { withTimeout(longPressTimeout) { waitForUpOrCancellation(pass = pass) } } catch (_: PointerEventTimeoutCancellationException) { // withTimeout throws exception if timeout has passed before block completes onLongPress.invoke(down.position) consumeUntilUp(pass) } } } /** * Consumes all pointer events until nothing is pressed and then returns. This method assumes that * something is currently pressed. */ private suspend fun AwaitPointerEventScope.consumeUntilUp( pass: PointerEventPass = PointerEventPass.Initial ) { do { val event = awaitPointerEvent(pass = pass) event.changes.fastForEach { it.consume() } } while (event.changes.fastAny { it.pressed }) } /** Consume all gestures on the initial pass so that child elements do not receive them. */ suspend fun PointerInputScope.consumeAllGestures() = coroutineScope { awaitEachGesture { Loading packages/SystemUI/res/values/strings.xml +2 −0 Original line number Diff line number Diff line Loading @@ -1089,6 +1089,8 @@ <string name="cta_label_to_open_widget_picker">Add more widgets</string> <!-- Text for the popup to be displayed after dismissing the CTA tile. [CHAR LIMIT=50] --> <string name="popup_on_dismiss_cta_tile_text">Long press to customize widgets</string> <!-- Text for the button to configure widgets after long press. [CHAR LIMIT=50] --> <string name="button_to_configure_widgets_text">Customize widgets</string> <!-- Label for the button which configures widgets [CHAR LIMIT=NONE] --> <string name="edit_widget">Edit widget</string> <!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] --> Loading Loading
packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +47 −0 Original line number Diff line number Diff line Loading @@ -97,11 +97,13 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Popup import androidx.core.view.setPadding import com.android.compose.modifiers.thenIf 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.Dimensions.CardOutlineWidth import com.android.systemui.communal.ui.compose.extensions.allowGestures import com.android.systemui.communal.ui.compose.extensions.detectLongPressGesture 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 Loading Loading @@ -132,6 +134,8 @@ fun CommunalHub( val removeButtonEnabled by remember { derivedStateOf { selectedIndex.value != null || reorderingWidgets } } val (isButtonToEditWidgetsShowing, setIsButtonToEditWidgetsShowing) = remember { mutableStateOf(false) } val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize) val contentOffset = beforeContentPadding(contentPadding).toOffset() Loading @@ -158,6 +162,11 @@ fun CommunalHub( } viewModel.setSelectedIndex(newIndex) } } .thenIf(!viewModel.isEditMode) { Modifier.pointerInput(Unit) { detectLongPressGesture { offset -> setIsButtonToEditWidgetsShowing(true) } } }, ) { CommunalHubLazyGrid( Loading Loading @@ -207,6 +216,16 @@ fun CommunalHub( PopupOnDismissCtaTile(viewModel::onHidePopupAfterDismissCta) } if (isButtonToEditWidgetsShowing) { ButtonToEditWidgets( onClick = { setIsButtonToEditWidgetsShowing(false) viewModel.onOpenWidgetEditor() }, onHide = { setIsButtonToEditWidgetsShowing(false) }, ) } // This spacer covers the edge of the LazyHorizontalGrid and prevents it from receiving // touches, so that the SceneTransitionLayout can intercept the touches and allow an edge // swipe back to the blank scene. Loading Loading @@ -413,6 +432,34 @@ private fun Toolbar( } } @Composable private fun ButtonToEditWidgets( onClick: () -> Unit, onHide: () -> Unit, ) { Popup(alignment = Alignment.TopCenter, offset = IntOffset(0, 40), onDismissRequest = onHide) { val colors = LocalAndroidColorScheme.current Button( modifier = Modifier.height(56.dp).background(colors.secondary, RoundedCornerShape(50.dp)), onClick = onClick, ) { Icon( imageVector = Icons.Outlined.Widgets, contentDescription = stringResource(R.string.button_to_configure_widgets_text), tint = colors.onSecondary, modifier = Modifier.size(20.dp) ) Spacer(modifier = Modifier.size(8.dp)) Text( text = stringResource(R.string.button_to_configure_widgets_text), style = MaterialTheme.typography.titleSmall, color = colors.onSecondary, ) } } } @Composable private fun PopupOnDismissCtaTile(onHidePopupAfterDismissCta: () -> Unit) { Popup( Loading
packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt +39 −0 Original line number Diff line number Diff line Loading @@ -20,9 +20,13 @@ 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.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach import kotlinx.coroutines.coroutineScope /** Loading @@ -44,6 +48,41 @@ suspend fun PointerInputScope.observeTapsWithoutConsuming( } } /** * Detect long press gesture and calls onLongPress when detected. The callback parameter receives an * Offset representing the position relative to the containing element. */ suspend fun PointerInputScope.detectLongPressGesture( pass: PointerEventPass = PointerEventPass.Initial, onLongPress: ((Offset) -> Unit), ) = coroutineScope { awaitEachGesture { val down = awaitFirstDown(pass = pass) val longPressTimeout = viewConfiguration.longPressTimeoutMillis // wait for first tap up or long press try { withTimeout(longPressTimeout) { waitForUpOrCancellation(pass = pass) } } catch (_: PointerEventTimeoutCancellationException) { // withTimeout throws exception if timeout has passed before block completes onLongPress.invoke(down.position) consumeUntilUp(pass) } } } /** * Consumes all pointer events until nothing is pressed and then returns. This method assumes that * something is currently pressed. */ private suspend fun AwaitPointerEventScope.consumeUntilUp( pass: PointerEventPass = PointerEventPass.Initial ) { do { val event = awaitPointerEvent(pass = pass) event.changes.fastForEach { it.consume() } } while (event.changes.fastAny { it.pressed }) } /** Consume all gestures on the initial pass so that child elements do not receive them. */ suspend fun PointerInputScope.consumeAllGestures() = coroutineScope { awaitEachGesture { Loading
packages/SystemUI/res/values/strings.xml +2 −0 Original line number Diff line number Diff line Loading @@ -1089,6 +1089,8 @@ <string name="cta_label_to_open_widget_picker">Add more widgets</string> <!-- Text for the popup to be displayed after dismissing the CTA tile. [CHAR LIMIT=50] --> <string name="popup_on_dismiss_cta_tile_text">Long press to customize widgets</string> <!-- Text for the button to configure widgets after long press. [CHAR LIMIT=50] --> <string name="button_to_configure_widgets_text">Customize widgets</string> <!-- Label for the button which configures widgets [CHAR LIMIT=NONE] --> <string name="edit_widget">Edit widget</string> <!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] --> Loading