Merge "Implement the QS footer actions in Compose (1/2)" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
ff576d7341
@@ -2331,6 +2331,7 @@
|
|||||||
<java-symbol type="drawable" name="scrubber_control_selector_holo" />
|
<java-symbol type="drawable" name="scrubber_control_selector_holo" />
|
||||||
<java-symbol type="drawable" name="scrubber_progress_horizontal_holo_dark" />
|
<java-symbol type="drawable" name="scrubber_progress_horizontal_holo_dark" />
|
||||||
<java-symbol type="drawable" name="progress_small_material" />
|
<java-symbol type="drawable" name="progress_small_material" />
|
||||||
|
<java-symbol type="drawable" name="ic_chevron_end" />
|
||||||
<java-symbol type="string" name="chooseUsbActivity" />
|
<java-symbol type="string" name="chooseUsbActivity" />
|
||||||
<java-symbol type="string" name="ext_media_badremoval_notification_message" />
|
<java-symbol type="string" name="ext_media_badremoval_notification_message" />
|
||||||
<java-symbol type="string" name="ext_media_badremoval_notification_title" />
|
<java-symbol type="string" name="ext_media_badremoval_notification_title" />
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import androidx.compose.foundation.BorderStroke
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
@@ -62,6 +63,7 @@ import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
|||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.graphics.drawscope.scale
|
import androidx.compose.ui.graphics.drawscope.scale
|
||||||
import androidx.compose.ui.layout.boundsInRoot
|
import androidx.compose.ui.layout.boundsInRoot
|
||||||
|
import androidx.compose.ui.layout.findRootCoordinates
|
||||||
import androidx.compose.ui.layout.layout
|
import androidx.compose.ui.layout.layout
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
@@ -118,12 +120,14 @@ fun Expandable(
|
|||||||
contentColor: Color = contentColorFor(color),
|
contentColor: Color = contentColorFor(color),
|
||||||
borderStroke: BorderStroke? = null,
|
borderStroke: BorderStroke? = null,
|
||||||
onClick: ((Expandable) -> Unit)? = null,
|
onClick: ((Expandable) -> Unit)? = null,
|
||||||
|
interactionSource: MutableInteractionSource? = null,
|
||||||
content: @Composable (Expandable) -> Unit,
|
content: @Composable (Expandable) -> Unit,
|
||||||
) {
|
) {
|
||||||
Expandable(
|
Expandable(
|
||||||
rememberExpandableController(color, shape, contentColor, borderStroke),
|
rememberExpandableController(color, shape, contentColor, borderStroke),
|
||||||
modifier,
|
modifier,
|
||||||
onClick,
|
onClick,
|
||||||
|
interactionSource,
|
||||||
content,
|
content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -158,6 +162,7 @@ fun Expandable(
|
|||||||
controller: ExpandableController,
|
controller: ExpandableController,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClick: ((Expandable) -> Unit)? = null,
|
onClick: ((Expandable) -> Unit)? = null,
|
||||||
|
interactionSource: MutableInteractionSource? = null,
|
||||||
content: @Composable (Expandable) -> Unit,
|
content: @Composable (Expandable) -> Unit,
|
||||||
) {
|
) {
|
||||||
val controller = controller as ExpandableControllerImpl
|
val controller = controller as ExpandableControllerImpl
|
||||||
@@ -190,6 +195,18 @@ fun Expandable(
|
|||||||
|
|
||||||
var thisExpandableSize by remember { mutableStateOf(Size.Zero) }
|
var thisExpandableSize by remember { mutableStateOf(Size.Zero) }
|
||||||
|
|
||||||
|
/** Set the current element size as this Expandable size. */
|
||||||
|
fun Modifier.updateExpandableSize(): Modifier {
|
||||||
|
return this.onGloballyPositioned { coords ->
|
||||||
|
thisExpandableSize =
|
||||||
|
coords
|
||||||
|
.findRootCoordinates()
|
||||||
|
// Make sure that we report the actual size, and not the visual/clipped one.
|
||||||
|
.localBoundingBoxOf(coords, clipBounds = false)
|
||||||
|
.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure we don't read animatorState directly here to avoid recomposition every time the
|
// Make sure we don't read animatorState directly here to avoid recomposition every time the
|
||||||
// state changes (i.e. every frame of the animation).
|
// state changes (i.e. every frame of the animation).
|
||||||
val isAnimating by remember {
|
val isAnimating by remember {
|
||||||
@@ -247,7 +264,7 @@ fun Expandable(
|
|||||||
controller.isDialogShowing.value -> {
|
controller.isDialogShowing.value -> {
|
||||||
Box(
|
Box(
|
||||||
modifier
|
modifier
|
||||||
.onGloballyPositioned { thisExpandableSize = it.boundsInRoot().size }
|
.updateExpandableSize()
|
||||||
.then(minInteractiveSizeModifier)
|
.then(minInteractiveSizeModifier)
|
||||||
.drawWithContent { /* Don't draw anything when the dialog is shown. */}
|
.drawWithContent { /* Don't draw anything when the dialog is shown. */}
|
||||||
.onGloballyPositioned {
|
.onGloballyPositioned {
|
||||||
@@ -258,18 +275,25 @@ fun Expandable(
|
|||||||
else -> {
|
else -> {
|
||||||
val clickModifier =
|
val clickModifier =
|
||||||
if (onClick != null) {
|
if (onClick != null) {
|
||||||
Modifier.clickable { onClick(controller.expandable) }
|
if (interactionSource != null) {
|
||||||
|
// If the caller provided an interaction source, then that means that they
|
||||||
|
// will draw the click indication themselves.
|
||||||
|
Modifier.clickable(interactionSource, indication = null) {
|
||||||
|
onClick(controller.expandable)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no interaction source is provided, we draw the default indication (a
|
||||||
|
// ripple) and make sure it's clipped by the expandable shape.
|
||||||
|
Modifier.clip(shape).clickable { onClick(controller.expandable) }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier
|
modifier
|
||||||
.onGloballyPositioned { thisExpandableSize = it.boundsInRoot().size }
|
.updateExpandableSize()
|
||||||
.then(minInteractiveSizeModifier)
|
.then(minInteractiveSizeModifier)
|
||||||
// Note that clip() *must* be above the clickModifier to properly clip the
|
|
||||||
// ripple.
|
|
||||||
.clip(shape)
|
|
||||||
.then(clickModifier)
|
.then(clickModifier)
|
||||||
.background(color, shape)
|
.background(color, shape)
|
||||||
.border(controller)
|
.border(controller)
|
||||||
|
|||||||
@@ -65,10 +65,12 @@ class AndroidColorScheme internal constructor(context: Context) {
|
|||||||
val colorForeground = getColor(context, R.attr.colorForeground)
|
val colorForeground = getColor(context, R.attr.colorForeground)
|
||||||
val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
|
val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
|
||||||
|
|
||||||
private fun getColor(context: Context, attr: Int): Color {
|
companion object {
|
||||||
val ta = context.obtainStyledAttributes(intArrayOf(attr))
|
fun getColor(context: Context, attr: Int): Color {
|
||||||
@ColorInt val color = ta.getColor(0, 0)
|
val ta = context.obtainStyledAttributes(intArrayOf(attr))
|
||||||
ta.recycle()
|
@ColorInt val color = ta.getColor(0, 0)
|
||||||
return Color(color)
|
ta.recycle()
|
||||||
|
return Color(color)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.compose.theme
|
||||||
|
|
||||||
|
import android.annotation.AttrRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
/** Read the [Color] from the given [attribute]. */
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
fun colorAttr(@AttrRes attribute: Int): Color {
|
||||||
|
return AndroidColorScheme.getColor(LocalContext.current, attribute)
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.common.ui.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.android.systemui.common.shared.model.ContentDescription
|
||||||
|
|
||||||
|
/** Returns the loaded [String] or `null` if there isn't one. */
|
||||||
|
@Composable
|
||||||
|
fun ContentDescription.load(): String? {
|
||||||
|
return when (this) {
|
||||||
|
is ContentDescription.Loaded -> description
|
||||||
|
is ContentDescription.Resource -> stringResource(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.common.ui.compose
|
||||||
|
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import com.android.systemui.common.shared.model.Icon
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon composable that draws [icon] using [tint].
|
||||||
|
*
|
||||||
|
* Note: You can use [Color.Unspecified] to disable the tint and keep the original icon colors.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun Icon(
|
||||||
|
icon: Icon,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
tint: Color = LocalContentColor.current,
|
||||||
|
) {
|
||||||
|
val contentDescription = icon.contentDescription?.load()
|
||||||
|
when (icon) {
|
||||||
|
is Icon.Loaded -> {
|
||||||
|
Icon(icon.drawable.toBitmap().asImageBitmap(), contentDescription, modifier, tint)
|
||||||
|
}
|
||||||
|
is Icon.Resource -> Icon(painterResource(icon.res), contentDescription, modifier, tint)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,7 +53,6 @@ import com.android.systemui.R
|
|||||||
import com.android.systemui.compose.theme.LocalAndroidColorScheme
|
import com.android.systemui.compose.theme.LocalAndroidColorScheme
|
||||||
import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
|
import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
|
||||||
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compose the screen associated to a [PeopleViewModel].
|
* Compose the screen associated to a [PeopleViewModel].
|
||||||
|
|||||||
@@ -0,0 +1,361 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.qs.footer.ui.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.indication
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
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.clip
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.layout.layout
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.constrainHeight
|
||||||
|
import androidx.compose.ui.unit.constrainWidth
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.em
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.animation.Expandable
|
||||||
|
import com.android.systemui.common.shared.model.Icon
|
||||||
|
import com.android.systemui.common.ui.compose.Icon
|
||||||
|
import com.android.systemui.compose.animation.Expandable
|
||||||
|
import com.android.systemui.compose.modifiers.background
|
||||||
|
import com.android.systemui.compose.theme.LocalAndroidColorScheme
|
||||||
|
import com.android.systemui.compose.theme.colorAttr
|
||||||
|
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel
|
||||||
|
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
|
||||||
|
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
|
||||||
|
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/** The Quick Settings footer actions row. */
|
||||||
|
@Composable
|
||||||
|
fun FooterActions(
|
||||||
|
viewModel: FooterActionsViewModel,
|
||||||
|
qsVisibilityLifecycleOwner: LifecycleOwner,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// Collect visibility and alphas as soon as we are composed, even when not visible.
|
||||||
|
val isVisible by viewModel.isVisible.collectAsState()
|
||||||
|
val alpha by viewModel.alpha.collectAsState()
|
||||||
|
val backgroundAlpha = viewModel.backgroundAlpha.collectAsState()
|
||||||
|
|
||||||
|
var security by remember { mutableStateOf<FooterActionsSecurityButtonViewModel?>(null) }
|
||||||
|
var foregroundServices by remember {
|
||||||
|
mutableStateOf<FooterActionsForegroundServicesButtonViewModel?>(null)
|
||||||
|
}
|
||||||
|
var userSwitcher by remember { mutableStateOf<FooterActionsButtonViewModel?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(
|
||||||
|
context,
|
||||||
|
qsVisibilityLifecycleOwner,
|
||||||
|
viewModel,
|
||||||
|
viewModel.security,
|
||||||
|
viewModel.foregroundServices,
|
||||||
|
viewModel.userSwitcher,
|
||||||
|
) {
|
||||||
|
launch {
|
||||||
|
// Listen for dialog requests as soon as we are composed, even when not visible.
|
||||||
|
viewModel.observeDeviceMonitoringDialogRequests(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for model changes only when QS are visible.
|
||||||
|
qsVisibilityLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
launch { viewModel.security.collect { security = it } }
|
||||||
|
launch { viewModel.foregroundServices.collect { foregroundServices = it } }
|
||||||
|
launch { viewModel.userSwitcher.collect { userSwitcher = it } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val backgroundColor = colorAttr(R.attr.underSurfaceColor)
|
||||||
|
val contentColor = LocalAndroidColorScheme.current.textColorPrimary
|
||||||
|
val backgroundTopRadius = dimensionResource(R.dimen.qs_corner_radius)
|
||||||
|
val backgroundModifier =
|
||||||
|
remember(
|
||||||
|
backgroundColor,
|
||||||
|
backgroundAlpha,
|
||||||
|
backgroundTopRadius,
|
||||||
|
) {
|
||||||
|
Modifier.background(
|
||||||
|
backgroundColor,
|
||||||
|
backgroundAlpha::value,
|
||||||
|
RoundedCornerShape(topStart = backgroundTopRadius, topEnd = backgroundTopRadius),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.graphicsLayer { this.alpha = alpha }
|
||||||
|
.drawWithContent {
|
||||||
|
if (isVisible) {
|
||||||
|
drawContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.then(backgroundModifier)
|
||||||
|
.padding(
|
||||||
|
top = dimensionResource(R.dimen.qs_footer_actions_top_padding),
|
||||||
|
bottom = dimensionResource(R.dimen.qs_footer_actions_bottom_padding),
|
||||||
|
)
|
||||||
|
.layout { measurable, constraints ->
|
||||||
|
// All buttons have a 4dp padding to increase their touch size. To be consistent
|
||||||
|
// with the View implementation, we want to left-most and right-most buttons to be
|
||||||
|
// visually aligned with the left and right sides of this row. So we let this
|
||||||
|
// component be 2*4dp wider and then offset it by -4dp to the start.
|
||||||
|
val inset = 4.dp.roundToPx()
|
||||||
|
val additionalWidth = inset * 2
|
||||||
|
val newConstraints =
|
||||||
|
if (constraints.hasBoundedWidth) {
|
||||||
|
constraints.copy(maxWidth = constraints.maxWidth + additionalWidth)
|
||||||
|
} else {
|
||||||
|
constraints
|
||||||
|
}
|
||||||
|
val placeable = measurable.measure(newConstraints)
|
||||||
|
|
||||||
|
val width = constraints.constrainWidth(placeable.width - additionalWidth)
|
||||||
|
val height = constraints.constrainHeight(placeable.height)
|
||||||
|
layout(width, height) { placeable.place(-inset, 0) }
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalContentColor provides contentColor,
|
||||||
|
) {
|
||||||
|
if (security == null && foregroundServices == null) {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
security?.let { SecurityButton(it, Modifier.weight(1f)) }
|
||||||
|
foregroundServices?.let { ForegroundServicesButton(it) }
|
||||||
|
userSwitcher?.let { IconButton(it) }
|
||||||
|
IconButton(viewModel.settings)
|
||||||
|
viewModel.power?.let { IconButton(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The security button. */
|
||||||
|
@Composable
|
||||||
|
private fun SecurityButton(
|
||||||
|
model: FooterActionsSecurityButtonViewModel,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val onClick: ((Expandable) -> Unit)? =
|
||||||
|
model.onClick?.let { onClick ->
|
||||||
|
val context = LocalContext.current
|
||||||
|
{ expandable -> onClick(context, expandable) }
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
model.icon,
|
||||||
|
model.text,
|
||||||
|
showNewDot = false,
|
||||||
|
onClick = onClick,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The foreground services button. */
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.ForegroundServicesButton(
|
||||||
|
model: FooterActionsForegroundServicesButtonViewModel,
|
||||||
|
) {
|
||||||
|
if (model.displayText) {
|
||||||
|
TextButton(
|
||||||
|
Icon.Resource(R.drawable.ic_info_outline, contentDescription = null),
|
||||||
|
model.text,
|
||||||
|
showNewDot = model.hasNewChanges,
|
||||||
|
onClick = model.onClick,
|
||||||
|
Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
NumberButton(
|
||||||
|
model.foregroundServicesCount,
|
||||||
|
showNewDot = model.hasNewChanges,
|
||||||
|
onClick = model.onClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A button with an icon. */
|
||||||
|
@Composable
|
||||||
|
private fun IconButton(
|
||||||
|
model: FooterActionsButtonViewModel,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Expandable(
|
||||||
|
color = colorAttr(model.backgroundColor),
|
||||||
|
shape = CircleShape,
|
||||||
|
onClick = model.onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
val tint = model.iconTint?.let { Color(it) } ?: Color.Unspecified
|
||||||
|
Icon(
|
||||||
|
model.icon,
|
||||||
|
tint = tint,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A button with a number an an optional dot (to indicate new changes). */
|
||||||
|
@Composable
|
||||||
|
private fun NumberButton(
|
||||||
|
number: Int,
|
||||||
|
showNewDot: Boolean,
|
||||||
|
onClick: (Expandable) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
// By default Expandable will show a ripple above its content when clicked, and clip the content
|
||||||
|
// with the shape of the expandable. In this case we also want to show a "new changes dot"
|
||||||
|
// outside of the shape, so we can't clip. To work around that we can pass our own interaction
|
||||||
|
// source and draw the ripple indication ourselves above the text but below the "new changes
|
||||||
|
// dot".
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
|
Expandable(
|
||||||
|
color = colorAttr(R.attr.offStateColor),
|
||||||
|
shape = CircleShape,
|
||||||
|
onClick = onClick,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Box(Modifier.size(40.dp)) {
|
||||||
|
Box(
|
||||||
|
Modifier.fillMaxSize()
|
||||||
|
.clip(CircleShape)
|
||||||
|
.indication(
|
||||||
|
interactionSource,
|
||||||
|
LocalIndication.current,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
number.toString(),
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = LocalAndroidColorScheme.current.textColorPrimary,
|
||||||
|
// TODO(b/242040009): This should only use a standard text style instead and
|
||||||
|
// should not override the text size.
|
||||||
|
fontSize = 18.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showNewDot) {
|
||||||
|
NewChangesDot(Modifier.align(Alignment.BottomEnd))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A dot that indicates new changes. */
|
||||||
|
@Composable
|
||||||
|
private fun NewChangesDot(modifier: Modifier = Modifier) {
|
||||||
|
val contentDescription = stringResource(R.string.fgs_dot_content_description)
|
||||||
|
val color = LocalAndroidColorScheme.current.colorAccentTertiary
|
||||||
|
|
||||||
|
Canvas(modifier.size(12.dp).semantics { this.contentDescription = contentDescription }) {
|
||||||
|
drawCircle(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A larger button with an icon, some text and an optional dot (to indicate new changes). */
|
||||||
|
@Composable
|
||||||
|
private fun TextButton(
|
||||||
|
icon: Icon,
|
||||||
|
text: String,
|
||||||
|
showNewDot: Boolean,
|
||||||
|
onClick: ((Expandable) -> Unit)?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Expandable(
|
||||||
|
shape = CircleShape,
|
||||||
|
color = colorAttr(R.attr.underSurfaceColor),
|
||||||
|
contentColor = LocalAndroidColorScheme.current.textColorSecondary,
|
||||||
|
borderStroke = BorderStroke(1.dp, LocalAndroidColorScheme.current.colorBackground),
|
||||||
|
modifier = modifier.padding(horizontal = 4.dp),
|
||||||
|
onClick = onClick,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier.padding(horizontal = dimensionResource(R.dimen.qs_footer_padding)),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(icon, Modifier.padding(end = 12.dp).size(20.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
Modifier.weight(1f),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
// TODO(b/242040009): Remove this letter spacing. We should only use the M3 text
|
||||||
|
// styles without modifying them.
|
||||||
|
letterSpacing = 0.01.em,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (showNewDot) {
|
||||||
|
NewChangesDot(Modifier.padding(start = 8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onClick != null) {
|
||||||
|
Icon(
|
||||||
|
painterResource(com.android.internal.R.drawable.ic_chevron_end),
|
||||||
|
contentDescription = null,
|
||||||
|
Modifier.padding(start = 8.dp).size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -237,7 +237,13 @@ object FooterActionsViewBinder {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonView.setBackgroundResource(model.background)
|
val backgroundResource =
|
||||||
|
when (model.backgroundColor) {
|
||||||
|
R.attr.offStateColor -> R.drawable.qs_footer_action_circle
|
||||||
|
com.android.internal.R.attr.colorAccent -> R.drawable.qs_footer_action_circle_color
|
||||||
|
else -> error("Unsupported icon background resource ${model.backgroundColor}")
|
||||||
|
}
|
||||||
|
buttonView.setBackgroundResource(backgroundResource)
|
||||||
buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) }
|
buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) }
|
||||||
|
|
||||||
val icon = model.icon
|
val icon = model.icon
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
|
|
||||||
package com.android.systemui.qs.footer.ui.viewmodel
|
package com.android.systemui.qs.footer.ui.viewmodel
|
||||||
|
|
||||||
import android.annotation.DrawableRes
|
import android.annotation.AttrRes
|
||||||
|
import android.annotation.ColorInt
|
||||||
import com.android.systemui.animation.Expandable
|
import com.android.systemui.animation.Expandable
|
||||||
import com.android.systemui.common.shared.model.Icon
|
import com.android.systemui.common.shared.model.Icon
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ import com.android.systemui.common.shared.model.Icon
|
|||||||
data class FooterActionsButtonViewModel(
|
data class FooterActionsButtonViewModel(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val icon: Icon,
|
val icon: Icon,
|
||||||
val iconTint: Int?,
|
@ColorInt val iconTint: Int?,
|
||||||
@DrawableRes val background: Int,
|
@AttrRes val backgroundColor: Int,
|
||||||
val onClick: (Expandable) -> Unit,
|
val onClick: (Expandable) -> Unit,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package com.android.systemui.qs.footer.ui.viewmodel
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.ContextThemeWrapper
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
@@ -49,12 +50,15 @@ import kotlinx.coroutines.flow.map
|
|||||||
|
|
||||||
/** A ViewModel for the footer actions. */
|
/** A ViewModel for the footer actions. */
|
||||||
class FooterActionsViewModel(
|
class FooterActionsViewModel(
|
||||||
@Application private val context: Context,
|
@Application appContext: Context,
|
||||||
private val footerActionsInteractor: FooterActionsInteractor,
|
private val footerActionsInteractor: FooterActionsInteractor,
|
||||||
private val falsingManager: FalsingManager,
|
private val falsingManager: FalsingManager,
|
||||||
private val globalActionsDialogLite: GlobalActionsDialogLite,
|
private val globalActionsDialogLite: GlobalActionsDialogLite,
|
||||||
showPowerButton: Boolean,
|
showPowerButton: Boolean,
|
||||||
) {
|
) {
|
||||||
|
/** The context themed with the Quick Settings colors. */
|
||||||
|
private val context = ContextThemeWrapper(appContext, R.style.Theme_SystemUI_QuickSettings)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the UI rendering this ViewModel should be visible. Note that even when this is false,
|
* Whether the UI rendering this ViewModel should be visible. Note that even when this is false,
|
||||||
* the UI should still participate to the layout it is included in (i.e. in the View world it
|
* the UI should still participate to the layout it is included in (i.e. in the View world it
|
||||||
@@ -142,7 +146,7 @@ class FooterActionsViewModel(
|
|||||||
ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
|
ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
|
||||||
),
|
),
|
||||||
iconTint = null,
|
iconTint = null,
|
||||||
R.drawable.qs_footer_action_circle,
|
backgroundColor = R.attr.offStateColor,
|
||||||
this::onSettingsButtonClicked,
|
this::onSettingsButtonClicked,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -160,7 +164,7 @@ class FooterActionsViewModel(
|
|||||||
context,
|
context,
|
||||||
com.android.internal.R.attr.textColorOnAccent,
|
com.android.internal.R.attr.textColorOnAccent,
|
||||||
),
|
),
|
||||||
R.drawable.qs_footer_action_circle_color,
|
backgroundColor = com.android.internal.R.attr.colorAccent,
|
||||||
this::onPowerButtonClicked,
|
this::onPowerButtonClicked,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -260,7 +264,7 @@ class FooterActionsViewModel(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
iconTint = null,
|
iconTint = null,
|
||||||
background = R.drawable.qs_footer_action_circle,
|
backgroundColor = R.attr.offStateColor,
|
||||||
onClick = this::onUserSwitcherClicked,
|
onClick = this::onUserSwitcherClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class FooterActionsViewModelTest : SysuiTestCase() {
|
|||||||
ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
|
ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(settings.background).isEqualTo(R.drawable.qs_footer_action_circle)
|
assertThat(settings.backgroundColor).isEqualTo(R.attr.offStateColor)
|
||||||
assertThat(settings.iconTint).isNull()
|
assertThat(settings.iconTint).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class FooterActionsViewModelTest : SysuiTestCase() {
|
|||||||
ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
|
ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(power.background).isEqualTo(R.drawable.qs_footer_action_circle_color)
|
assertThat(power.backgroundColor).isEqualTo(com.android.internal.R.attr.colorAccent)
|
||||||
assertThat(power.iconTint)
|
assertThat(power.iconTint)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
Utils.getColorAttrDefaultColor(
|
Utils.getColorAttrDefaultColor(
|
||||||
@@ -170,7 +170,7 @@ class FooterActionsViewModelTest : SysuiTestCase() {
|
|||||||
assertThat(userSwitcher).isNotNull()
|
assertThat(userSwitcher).isNotNull()
|
||||||
assertThat(userSwitcher!!.icon)
|
assertThat(userSwitcher!!.icon)
|
||||||
.isEqualTo(Icon.Loaded(picture, ContentDescription.Loaded("Signed in as foo")))
|
.isEqualTo(Icon.Loaded(picture, ContentDescription.Loaded("Signed in as foo")))
|
||||||
assertThat(userSwitcher.background).isEqualTo(R.drawable.qs_footer_action_circle)
|
assertThat(userSwitcher.backgroundColor).isEqualTo(R.attr.offStateColor)
|
||||||
|
|
||||||
// Change the current user name.
|
// Change the current user name.
|
||||||
userSwitcherControllerWrapper.currentUserName = "bar"
|
userSwitcherControllerWrapper.currentUserName = "bar"
|
||||||
|
|||||||
Reference in New Issue
Block a user