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

Commit 8073ecaa authored by Pat Manning's avatar Pat Manning Committed by Android (Google) Code Review
Browse files

Merge "Add dialog factory method for creating draggable bottom sheet dialogs in compose." into main

parents 7875655e b6c16a61
Loading
Loading
Loading
Loading
+120 −6
Original line number Diff line number Diff line
@@ -16,34 +16,58 @@

package com.android.systemui.statusbar.phone

import android.app.Dialog
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import androidx.annotation.GravityInt
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified
import com.android.compose.theme.PlatformTheme
import com.android.systemui.keyboard.shortcut.ui.composable.hasCompactWindowSize
import com.android.systemui.res.R
import kotlin.math.roundToInt

/**
 * Create a [SystemUIDialog] with the given [content].
@@ -97,6 +121,9 @@ fun SystemUIDialogFactory.createBottomSheet(
    theme: Int = R.style.Theme_SystemUI_BottomSheet,
    dismissOnDeviceLock: Boolean = SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
    content: @Composable (SystemUIDialog) -> Unit,
    isDraggable: Boolean = true,
    // TODO(b/337205027): remove maxWidth parameter when aligned to M3 spec
    maxWidth: Dp = Dp.Unspecified,
): ComponentSystemUIDialog {
    return create(
        context = context,
@@ -104,9 +131,49 @@ fun SystemUIDialogFactory.createBottomSheet(
        dismissOnDeviceLock = dismissOnDeviceLock,
        delegate = EdgeToEdgeDialogDelegate(),
        content = { dialog ->
            val dragState =
                if (isDraggable)
                    remember { AnchoredDraggableState(initialValue = DragAnchors.Start) }
                else null
            val interactionSource =
                if (isDraggable) remember { MutableInteractionSource() } else null
            if (dragState != null) {
                val isDragged by interactionSource!!.collectIsDraggedAsState()
                LaunchedEffect(dragState.currentValue, isDragged) {
                    if (!isDragged && dragState.currentValue == DragAnchors.End) dialog.dismiss()
                }
            }
            Box(
                modifier = Modifier.bottomSheetClickable { dialog.dismiss() },
                contentAlignment = Alignment.BottomCenter
                modifier =
                    Modifier.bottomSheetClickable { dialog.dismiss() }
                        .then(
                            if (isDraggable)
                                Modifier.anchoredDraggable(
                                        state = dragState!!,
                                        interactionSource = interactionSource,
                                        orientation = Orientation.Vertical,
                                        flingBehavior =
                                            AnchoredDraggableDefaults.flingBehavior(
                                                state = dragState
                                            ),
                                    )
                                    .offset {
                                        IntOffset(x = 0, y = dragState.requireOffset().roundToInt())
                                    }
                                    .onSizeChanged { layoutSize ->
                                        val dragEndPoint = layoutSize.height - dialog.height
                                        dragState.updateAnchors(
                                            DraggableAnchors {
                                                DragAnchors.entries.forEach { anchor ->
                                                    anchor at dragEndPoint * anchor.fraction
                                                }
                                            }
                                        )
                                    }
                                    .padding(top = draggableTopPadding())
                            else Modifier // No-Op
                        ),
                contentAlignment = Alignment.BottomCenter,
            ) {
                val radius = dimensionResource(R.dimen.bottom_sheet_corner_radius)
                Surface(
@@ -114,8 +181,11 @@ fun SystemUIDialogFactory.createBottomSheet(
                        Modifier.bottomSheetPaddings()
                            // consume input so it doesn't get to the parent Composable
                            .bottomSheetClickable {}
                            // TODO(b/337205027) change width
                            .widthIn(max = 800.dp),
                            .widthIn(
                                max =
                                    if (maxWidth.isSpecified) maxWidth
                                    else DraggableBottomSheet.MaxWidth
                            ),
                    shape = RoundedCornerShape(topStart = radius, topEnd = radius),
                    color = MaterialTheme.colorScheme.surfaceContainer,
                ) {
@@ -127,14 +197,29 @@ fun SystemUIDialogFactory.createBottomSheet(
                                }
                        )
                    ) {
                        if (isDraggable) {
                            Column(
                                Modifier.wrapContentWidth(Alignment.CenterHorizontally),
                                horizontalAlignment = Alignment.CenterHorizontally,
                            ) {
                                DragHandle(dialog)
                                content(dialog)
                            }
                        } else {
                            content(dialog)
                        }
                    }
                }
            }
        },
    )
}

private enum class DragAnchors(val fraction: Float) {
    Start(0f),
    End(1f),
}

private fun SystemUIDialogFactory.create(
    context: Context,
    theme: Int,
@@ -177,7 +262,7 @@ private fun Modifier.bottomSheetPaddings(): Modifier {
        padding(
            start = insets.getLeft(this, LocalLayoutDirection.current).toDp() + horizontalPadding,
            top = insets.getTop(this).toDp(),
            end = insets.getRight(this, LocalLayoutDirection.current).toDp() + horizontalPadding
            end = insets.getRight(this, LocalLayoutDirection.current).toDp() + horizontalPadding,
        )
    }
}
@@ -191,3 +276,32 @@ private fun Modifier.bottomSheetPaddings(): Modifier {
@Composable
private fun Modifier.bottomSheetClickable(onClick: () -> Unit) =
    pointerInput(onClick) { detectTapGestures { onClick() } }

@Composable
private fun DragHandle(dialog: Dialog) {
    // TODO(b/373340318): Rename drag handle string resource.
    val dragHandleContentDescription =
        stringResource(id = R.string.shortcut_helper_content_description_drag_handle)
    Surface(
        modifier =
            Modifier.padding(top = 16.dp, bottom = 6.dp)
                .semantics { contentDescription = dragHandleContentDescription }
                .clickable { dialog.dismiss() },
        color = MaterialTheme.colorScheme.outlineVariant,
        shape = MaterialTheme.shapes.extraLarge,
    ) {
        Box(Modifier.size(width = 32.dp, height = 4.dp))
    }
}

@Composable
private fun draggableTopPadding(): Dp {
    return if (hasCompactWindowSize()) DraggableBottomSheet.DefaultTopPadding
    else DraggableBottomSheet.LargeScreenTopPadding
}

private object DraggableBottomSheet {
    val DefaultTopPadding = 64.dp
    val LargeScreenTopPadding = 72.dp
    val MaxWidth = 640.dp
}
+14 −34
Original line number Diff line number Diff line
@@ -31,7 +31,6 @@ import androidx.compose.ui.semantics.paneTitle
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.theme.PlatformTheme
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.res.R
import com.android.systemui.volume.panel.ui.layout.ComponentsLayout
@@ -43,38 +42,28 @@ private const val VolumePanelTestTag = "VolumePanel"
private val padding = 24.dp

@Composable
fun VolumePanelRoot(
    viewModel: VolumePanelViewModel,
    modifier: Modifier = Modifier,
) {
fun VolumePanelRoot(viewModel: VolumePanelViewModel, modifier: Modifier = Modifier) {
    val accessibilityTitle = stringResource(R.string.accessibility_volume_settings)
    val state: VolumePanelState by viewModel.volumePanelState.collectAsStateWithLifecycle()
    val components by viewModel.componentsLayout.collectAsStateWithLifecycle()

    with(VolumePanelComposeScope(state)) {
        components?.let { componentsState ->
            PlatformTheme {
            Components(
                componentsState,
                modifier
                    .sysuiResTag(VolumePanelTestTag)
                    .semantics { paneTitle = accessibilityTitle }
                        .padding(
                            start = padding,
                            top = padding,
                            end = padding,
                            bottom = 20.dp,
                        )
                    .padding(start = padding, top = padding, end = padding, bottom = 20.dp),
            )
        }
    }
}
}

@Composable
private fun VolumePanelComposeScope.Components(
    layout: ComponentsLayout,
    modifier: Modifier = Modifier
    modifier: Modifier = Modifier,
) {
    val arrangement: Arrangement.Vertical =
        if (isLargeScreen) {
@@ -82,14 +71,11 @@ private fun VolumePanelComposeScope.Components(
        } else {
            if (isPortrait) Arrangement.spacedBy(padding) else Arrangement.spacedBy(4.dp)
        }
    Column(
        modifier = modifier,
        verticalArrangement = arrangement,
    ) {
    Column(modifier = modifier, verticalArrangement = arrangement) {
        if (isPortrait || isLargeScreen) {
            VerticalVolumePanelContent(
                modifier = Modifier.weight(weight = 1f, fill = false),
                layout = layout
                layout = layout,
            )
        } else {
            HorizontalVolumePanelContent(
@@ -97,23 +83,17 @@ private fun VolumePanelComposeScope.Components(
                layout = layout,
            )
        }
        BottomBar(
            modifier = Modifier,
            layout = layout,
        )
        BottomBar(modifier = Modifier, layout = layout)
    }
}

@Composable
private fun VolumePanelComposeScope.BottomBar(
    layout: ComponentsLayout,
    modifier: Modifier = Modifier
    modifier: Modifier = Modifier,
) {
    if (layout.bottomBarComponent.isVisible) {
        Box(
            modifier = modifier.fillMaxWidth(),
            contentAlignment = Alignment.Center,
        ) {
        Box(modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
            with(layout.bottomBarComponent.component as ComposeVolumePanelUiComponent) {
                Content(Modifier)
            }
+5 −1
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.provider.Settings
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.unit.dp
import com.android.internal.logging.UiEventLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -89,7 +90,7 @@ constructor(
            VolumePanelRoute.SETTINGS_VOLUME_PANEL ->
                activityStarter.startActivity(
                    /* intent= */ Intent(Settings.Panel.ACTION_VOLUME),
                    /* dismissShade= */ true
                    /* dismissShade= */ true,
                )
            VolumePanelRoute.SYSTEM_UI_VOLUME_PANEL ->
                volumePanelFactory.create(aboveStatusBar = true, view = null)
@@ -122,6 +123,9 @@ constructor(
                    remember(coroutineScope) { viewModelFactory.create(coroutineScope) }
                )
            },
            isDraggable = false,
            // TODO(b/337205027) change maxWidth
            maxWidth = 800.dp,
        )
    }
}