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_progress_horizontal_holo_dark" />
|
||||
<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="ext_media_badremoval_notification_message" />
|
||||
<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.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.scale
|
||||
import androidx.compose.ui.layout.boundsInRoot
|
||||
import androidx.compose.ui.layout.findRootCoordinates
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
@@ -118,12 +120,14 @@ fun Expandable(
|
||||
contentColor: Color = contentColorFor(color),
|
||||
borderStroke: BorderStroke? = null,
|
||||
onClick: ((Expandable) -> Unit)? = null,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
content: @Composable (Expandable) -> Unit,
|
||||
) {
|
||||
Expandable(
|
||||
rememberExpandableController(color, shape, contentColor, borderStroke),
|
||||
modifier,
|
||||
onClick,
|
||||
interactionSource,
|
||||
content,
|
||||
)
|
||||
}
|
||||
@@ -158,6 +162,7 @@ fun Expandable(
|
||||
controller: ExpandableController,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: ((Expandable) -> Unit)? = null,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
content: @Composable (Expandable) -> Unit,
|
||||
) {
|
||||
val controller = controller as ExpandableControllerImpl
|
||||
@@ -190,6 +195,18 @@ fun Expandable(
|
||||
|
||||
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
|
||||
// state changes (i.e. every frame of the animation).
|
||||
val isAnimating by remember {
|
||||
@@ -247,7 +264,7 @@ fun Expandable(
|
||||
controller.isDialogShowing.value -> {
|
||||
Box(
|
||||
modifier
|
||||
.onGloballyPositioned { thisExpandableSize = it.boundsInRoot().size }
|
||||
.updateExpandableSize()
|
||||
.then(minInteractiveSizeModifier)
|
||||
.drawWithContent { /* Don't draw anything when the dialog is shown. */}
|
||||
.onGloballyPositioned {
|
||||
@@ -258,18 +275,25 @@ fun Expandable(
|
||||
else -> {
|
||||
val clickModifier =
|
||||
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 {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier
|
||||
.onGloballyPositioned { thisExpandableSize = it.boundsInRoot().size }
|
||||
.updateExpandableSize()
|
||||
.then(minInteractiveSizeModifier)
|
||||
// Note that clip() *must* be above the clickModifier to properly clip the
|
||||
// ripple.
|
||||
.clip(shape)
|
||||
.then(clickModifier)
|
||||
.background(color, shape)
|
||||
.border(controller)
|
||||
|
||||
@@ -65,10 +65,12 @@ class AndroidColorScheme internal constructor(context: Context) {
|
||||
val colorForeground = getColor(context, R.attr.colorForeground)
|
||||
val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
|
||||
|
||||
private fun getColor(context: Context, attr: Int): Color {
|
||||
val ta = context.obtainStyledAttributes(intArrayOf(attr))
|
||||
@ColorInt val color = ta.getColor(0, 0)
|
||||
ta.recycle()
|
||||
return Color(color)
|
||||
companion object {
|
||||
fun getColor(context: Context, attr: Int): Color {
|
||||
val ta = context.obtainStyledAttributes(intArrayOf(attr))
|
||||
@ColorInt val color = ta.getColor(0, 0)
|
||||
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.people.ui.viewmodel.PeopleTileViewModel
|
||||
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
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)) }
|
||||
|
||||
val icon = model.icon
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
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.common.shared.model.Icon
|
||||
|
||||
@@ -27,7 +28,7 @@ import com.android.systemui.common.shared.model.Icon
|
||||
data class FooterActionsButtonViewModel(
|
||||
val id: Int,
|
||||
val icon: Icon,
|
||||
val iconTint: Int?,
|
||||
@DrawableRes val background: Int,
|
||||
@ColorInt val iconTint: Int?,
|
||||
@AttrRes val backgroundColor: Int,
|
||||
val onClick: (Expandable) -> Unit,
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.android.systemui.qs.footer.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.ContextThemeWrapper
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@@ -49,12 +50,15 @@ import kotlinx.coroutines.flow.map
|
||||
|
||||
/** A ViewModel for the footer actions. */
|
||||
class FooterActionsViewModel(
|
||||
@Application private val context: Context,
|
||||
@Application appContext: Context,
|
||||
private val footerActionsInteractor: FooterActionsInteractor,
|
||||
private val falsingManager: FalsingManager,
|
||||
private val globalActionsDialogLite: GlobalActionsDialogLite,
|
||||
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,
|
||||
* 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)
|
||||
),
|
||||
iconTint = null,
|
||||
R.drawable.qs_footer_action_circle,
|
||||
backgroundColor = R.attr.offStateColor,
|
||||
this::onSettingsButtonClicked,
|
||||
)
|
||||
|
||||
@@ -160,7 +164,7 @@ class FooterActionsViewModel(
|
||||
context,
|
||||
com.android.internal.R.attr.textColorOnAccent,
|
||||
),
|
||||
R.drawable.qs_footer_action_circle_color,
|
||||
backgroundColor = com.android.internal.R.attr.colorAccent,
|
||||
this::onPowerButtonClicked,
|
||||
)
|
||||
} else {
|
||||
@@ -260,7 +264,7 @@ class FooterActionsViewModel(
|
||||
),
|
||||
),
|
||||
iconTint = null,
|
||||
background = R.drawable.qs_footer_action_circle,
|
||||
backgroundColor = R.attr.offStateColor,
|
||||
onClick = this::onUserSwitcherClicked,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class FooterActionsViewModelTest : SysuiTestCase() {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ class FooterActionsViewModelTest : SysuiTestCase() {
|
||||
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)
|
||||
.isEqualTo(
|
||||
Utils.getColorAttrDefaultColor(
|
||||
@@ -170,7 +170,7 @@ class FooterActionsViewModelTest : SysuiTestCase() {
|
||||
assertThat(userSwitcher).isNotNull()
|
||||
assertThat(userSwitcher!!.icon)
|
||||
.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.
|
||||
userSwitcherControllerWrapper.currentUserName = "bar"
|
||||
|
||||
Reference in New Issue
Block a user