Merge "Implement the People UI in Compose (1/2)" into tm-qpr-dev am: ea08a5217b
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19497836 Change-Id: I84cae536c4090c658e32bec889577c32265a3141 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
committed by
Automerger Merge Worker
commit
9f83460516
@@ -30,6 +30,7 @@ android_library {
|
||||
],
|
||||
|
||||
static_libs: [
|
||||
"SystemUI-core",
|
||||
"SystemUIComposeCore",
|
||||
|
||||
"androidx.compose.runtime_runtime",
|
||||
|
||||
@@ -16,7 +16,38 @@
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
|
||||
package="com.android.systemui.compose.features">
|
||||
|
||||
|
||||
<application
|
||||
android:name="android.app.Application"
|
||||
android:appComponentFactory="androidx.core.app.AppComponentFactory"
|
||||
tools:replace="android:name,android:appComponentFactory">
|
||||
<!-- Disable providers from SystemUI -->
|
||||
<provider android:name="com.android.systemui.keyguard.KeyguardSliceProvider"
|
||||
android:authorities="com.android.systemui.test.keyguard.disabled"
|
||||
android:enabled="false"
|
||||
tools:replace="android:authorities"
|
||||
tools:node="remove" />
|
||||
<provider android:name="com.google.android.systemui.keyguard.KeyguardSliceProviderGoogle"
|
||||
android:authorities="com.android.systemui.test.keyguard.disabled"
|
||||
android:enabled="false"
|
||||
tools:replace="android:authorities"
|
||||
tools:node="remove" />
|
||||
<provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
|
||||
android:authorities="com.android.systemui.test.keyguard.clock.disabled"
|
||||
android:enabled="false"
|
||||
tools:replace="android:authorities"
|
||||
tools:node="remove" />
|
||||
<provider android:name="com.android.systemui.people.PeopleProvider"
|
||||
android:authorities="com.android.systemui.test.people.disabled"
|
||||
android:enabled="false"
|
||||
tools:replace="android:authorities"
|
||||
tools:node="remove" />
|
||||
<provider android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="com.android.systemui.test.fileprovider.disabled"
|
||||
android:enabled="false"
|
||||
tools:replace="android:authorities"
|
||||
tools:node="remove"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* 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.people.ui.compose
|
||||
|
||||
import android.annotation.StringRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
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].
|
||||
*
|
||||
* @param viewModel the [PeopleViewModel] that should be composed.
|
||||
* @param onResult the callback called with the result of this screen. Callers should usually finish
|
||||
* the Activity/Fragment/View hosting this Composable once a result is available.
|
||||
*/
|
||||
@Composable
|
||||
fun PeopleScreen(
|
||||
viewModel: PeopleViewModel,
|
||||
onResult: (PeopleViewModel.Result) -> Unit,
|
||||
) {
|
||||
val priorityTiles by viewModel.priorityTiles.collectAsState()
|
||||
val recentTiles by viewModel.recentTiles.collectAsState()
|
||||
|
||||
// Make sure to refresh the tiles/conversations when the lifecycle is resumed, so that it
|
||||
// updates them when going back to the Activity after leaving it.
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
LaunchedEffect(lifecycleOwner, viewModel) {
|
||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
viewModel.onTileRefreshRequested()
|
||||
}
|
||||
}
|
||||
|
||||
// Call [onResult] this activity when the ViewModel tells us so.
|
||||
LaunchedEffect(viewModel.result) {
|
||||
viewModel.result.collect { result ->
|
||||
if (result != null) {
|
||||
viewModel.clearResult()
|
||||
onResult(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure to use the Android colors and not the default Material3 colors to have the exact
|
||||
// same colors as the View implementation.
|
||||
val androidColors = LocalAndroidColorScheme.current
|
||||
Surface(
|
||||
color = androidColors.colorBackground,
|
||||
contentColor = androidColors.textColorPrimary,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) {
|
||||
PeopleScreenWithConversations(priorityTiles, recentTiles, viewModel::onTileClicked)
|
||||
} else {
|
||||
PeopleScreenEmpty(viewModel::onUserJourneyCancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PeopleScreenWithConversations(
|
||||
priorityTiles: List<PeopleTileViewModel>,
|
||||
recentTiles: List<PeopleTileViewModel>,
|
||||
onTileClicked: (PeopleTileViewModel) -> Unit,
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().padding(PeopleSpacePadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.select_conversation_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
stringResource(R.string.select_conversation_text),
|
||||
Modifier.padding(horizontal = 24.dp),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
top = 16.dp,
|
||||
bottom = PeopleSpacePadding,
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
)
|
||||
) {
|
||||
ConversationList(R.string.priority_conversations, priorityTiles, onTileClicked)
|
||||
item { Spacer(Modifier.height(35.dp)) }
|
||||
ConversationList(R.string.recent_conversations, recentTiles, onTileClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.ConversationList(
|
||||
@StringRes headerTextResource: Int,
|
||||
tiles: List<PeopleTileViewModel>,
|
||||
onTileClicked: (PeopleTileViewModel) -> Unit
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(headerTextResource),
|
||||
Modifier.padding(start = 16.dp),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
|
||||
tiles.forEachIndexed { index, tile ->
|
||||
if (index > 0) {
|
||||
item {
|
||||
Divider(
|
||||
color = LocalAndroidColorScheme.current.colorBackground,
|
||||
thickness = 2.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item(tile.key.toString()) {
|
||||
Tile(
|
||||
tile,
|
||||
onTileClicked,
|
||||
withTopCornerRadius = index == 0,
|
||||
withBottomCornerRadius = index == tiles.lastIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Tile(
|
||||
tile: PeopleTileViewModel,
|
||||
onTileClicked: (PeopleTileViewModel) -> Unit,
|
||||
withTopCornerRadius: Boolean,
|
||||
withBottomCornerRadius: Boolean,
|
||||
) {
|
||||
val androidColors = LocalAndroidColorScheme.current
|
||||
val cornerRadius = dimensionResource(R.dimen.people_space_widget_radius)
|
||||
val topCornerRadius = if (withTopCornerRadius) cornerRadius else 0.dp
|
||||
val bottomCornerRadius = if (withBottomCornerRadius) cornerRadius else 0.dp
|
||||
|
||||
Surface(
|
||||
color = androidColors.colorSurface,
|
||||
contentColor = androidColors.textColorPrimary,
|
||||
shape =
|
||||
RoundedCornerShape(
|
||||
topStart = topCornerRadius,
|
||||
topEnd = topCornerRadius,
|
||||
bottomStart = bottomCornerRadius,
|
||||
bottomEnd = bottomCornerRadius,
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().clickable { onTileClicked(tile) }.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
tile.icon.asImageBitmap(),
|
||||
// TODO(b/238993727): Add a content description.
|
||||
contentDescription = null,
|
||||
Modifier.size(dimensionResource(R.dimen.avatar_size_for_medium)),
|
||||
)
|
||||
|
||||
Text(
|
||||
tile.username ?: "",
|
||||
Modifier.padding(horizontal = 16.dp),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The padding applied to the PeopleSpace screen. */
|
||||
internal val PeopleSpacePadding = 24.dp
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.people.ui.compose
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.compose.theme.LocalAndroidColorScheme
|
||||
|
||||
@Composable
|
||||
internal fun PeopleScreenEmpty(
|
||||
onGotItClicked: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxSize().padding(PeopleSpacePadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.select_conversation_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(50.dp))
|
||||
|
||||
Text(
|
||||
stringResource(R.string.no_conversations_text),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
ExampleTile()
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
val androidColors = LocalAndroidColorScheme.current
|
||||
Button(
|
||||
onGotItClicked,
|
||||
Modifier.fillMaxWidth().defaultMinSize(minHeight = 56.dp),
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = androidColors.colorAccentPrimary,
|
||||
contentColor = androidColors.textColorOnAccent,
|
||||
)
|
||||
) { Text(stringResource(R.string.got_it)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExampleTile() {
|
||||
val androidColors = LocalAndroidColorScheme.current
|
||||
Surface(
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
color = androidColors.colorSurface,
|
||||
contentColor = androidColors.textColorPrimary,
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(vertical = 20.dp, horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
// TODO(b/238993727): Add a content description.
|
||||
Image(
|
||||
painterResource(R.drawable.ic_avatar_with_badge),
|
||||
contentDescription = null,
|
||||
Modifier.size(40.dp),
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
stringResource(R.string.empty_user_name),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(24.dp))
|
||||
|
||||
Text(
|
||||
stringResource(R.string.empty_status),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ android_library {
|
||||
|
||||
srcs: [
|
||||
"src/**/*.kt",
|
||||
":SystemUI-tests-utils",
|
||||
],
|
||||
|
||||
resource_dirs: [
|
||||
@@ -45,6 +46,14 @@ android_library {
|
||||
"androidx.navigation_navigation-compose",
|
||||
|
||||
"androidx.appcompat_appcompat",
|
||||
|
||||
// TODO(b/240431193): Remove the dependencies and depend on
|
||||
// SystemUI-test-utils directly.
|
||||
"androidx.test.runner",
|
||||
"mockito-target-extended-minus-junit4",
|
||||
"testables",
|
||||
"truth-prebuilt",
|
||||
"androidx.test.uiautomator",
|
||||
],
|
||||
|
||||
kotlincflags: ["-Xjvm-default=all"],
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -33,6 +33,28 @@ object GalleryAppScreens {
|
||||
val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
|
||||
val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() }
|
||||
|
||||
val PeopleEmpty =
|
||||
ChildScreen("people_empty") { navController ->
|
||||
EmptyPeopleScreen(onResult = { navController.popBackStack() })
|
||||
}
|
||||
val PeopleFew =
|
||||
ChildScreen("people_few") { navController ->
|
||||
FewPeopleScreen(onResult = { navController.popBackStack() })
|
||||
}
|
||||
val PeopleFull =
|
||||
ChildScreen("people_full") { navController ->
|
||||
FullPeopleScreen(onResult = { navController.popBackStack() })
|
||||
}
|
||||
val People =
|
||||
ParentScreen(
|
||||
"people",
|
||||
mapOf(
|
||||
"Empty" to PeopleEmpty,
|
||||
"Few" to PeopleFew,
|
||||
"Full" to PeopleFull,
|
||||
)
|
||||
)
|
||||
|
||||
val Home =
|
||||
ParentScreen(
|
||||
"home",
|
||||
@@ -41,20 +63,21 @@ object GalleryAppScreens {
|
||||
"Material colors" to MaterialColors,
|
||||
"Android colors" to AndroidColors,
|
||||
"Example feature" to ExampleFeature,
|
||||
"People" to People,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/** The main content of the app, that shows [GalleryAppScreens.Home] by default. */
|
||||
@Composable
|
||||
private fun MainContent() {
|
||||
private fun MainContent(onControlToggleRequested: () -> Unit) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
val navController = rememberNavController()
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = GalleryAppScreens.Home.identifier,
|
||||
) {
|
||||
screen(GalleryAppScreens.Home, navController)
|
||||
screen(GalleryAppScreens.Home, navController, onControlToggleRequested)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +92,7 @@ fun GalleryApp(
|
||||
onChangeTheme: () -> Unit,
|
||||
) {
|
||||
val systemFontScale = LocalDensity.current.fontScale
|
||||
var fontScale: FontScale by remember {
|
||||
var fontScale: FontScale by rememberSaveable {
|
||||
mutableStateOf(
|
||||
FontScale.values().firstOrNull { it.scale == systemFontScale } ?: FontScale.Normal
|
||||
)
|
||||
@@ -87,7 +110,7 @@ fun GalleryApp(
|
||||
}
|
||||
|
||||
val systemLayoutDirection = LocalLayoutDirection.current
|
||||
var layoutDirection by remember { mutableStateOf(systemLayoutDirection) }
|
||||
var layoutDirection by rememberSaveable { mutableStateOf(systemLayoutDirection) }
|
||||
val onChangeLayoutDirection = {
|
||||
layoutDirection =
|
||||
when (layoutDirection) {
|
||||
@@ -105,19 +128,24 @@ fun GalleryApp(
|
||||
Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
Column(Modifier.fillMaxSize().systemBarsPadding().padding(16.dp)) {
|
||||
ConfigurationControls(
|
||||
theme,
|
||||
fontScale,
|
||||
layoutDirection,
|
||||
onChangeTheme,
|
||||
onChangeLayoutDirection,
|
||||
onChangeFontScale,
|
||||
)
|
||||
Column(Modifier.fillMaxSize().systemBarsPadding()) {
|
||||
var showControls by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
if (showControls) {
|
||||
ConfigurationControls(
|
||||
theme,
|
||||
fontScale,
|
||||
layoutDirection,
|
||||
onChangeTheme,
|
||||
onChangeLayoutDirection,
|
||||
onChangeFontScale,
|
||||
Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
MainContent()
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
MainContent(onControlToggleRequested = { showControls = !showControls })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.gallery
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.android.systemui.people.emptyPeopleSpaceViewModel
|
||||
import com.android.systemui.people.fewPeopleSpaceViewModel
|
||||
import com.android.systemui.people.fullPeopleSpaceViewModel
|
||||
import com.android.systemui.people.ui.compose.PeopleScreen
|
||||
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
||||
|
||||
@Composable
|
||||
fun EmptyPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
|
||||
val context = LocalContext.current.applicationContext
|
||||
val viewModel = emptyPeopleSpaceViewModel(context)
|
||||
PeopleScreen(viewModel, onResult)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FewPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
|
||||
val context = LocalContext.current.applicationContext
|
||||
val viewModel = fewPeopleSpaceViewModel(context)
|
||||
PeopleScreen(viewModel, onResult)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FullPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
|
||||
val context = LocalContext.current.applicationContext
|
||||
val viewModel = fullPeopleSpaceViewModel(context)
|
||||
PeopleScreen(viewModel, onResult)
|
||||
}
|
||||
@@ -52,17 +52,29 @@ class ChildScreen(
|
||||
) : Screen(identifier)
|
||||
|
||||
/** Create the navigation graph for [screen]. */
|
||||
fun NavGraphBuilder.screen(screen: Screen, navController: NavController) {
|
||||
fun NavGraphBuilder.screen(
|
||||
screen: Screen,
|
||||
navController: NavController,
|
||||
onControlToggleRequested: () -> Unit,
|
||||
) {
|
||||
when (screen) {
|
||||
is ChildScreen -> composable(screen.identifier) { screen.content(navController) }
|
||||
is ParentScreen -> {
|
||||
val menuRoute = "${screen.identifier}_menu"
|
||||
navigation(startDestination = menuRoute, route = screen.identifier) {
|
||||
// The menu to navigate to one of the children screens.
|
||||
composable(menuRoute) { ScreenMenu(screen, navController) }
|
||||
composable(menuRoute) {
|
||||
ScreenMenu(screen, navController, onControlToggleRequested)
|
||||
}
|
||||
|
||||
// The content of the child screens.
|
||||
screen.children.forEach { (_, child) -> screen(child, navController) }
|
||||
screen.children.forEach { (_, child) ->
|
||||
screen(
|
||||
child,
|
||||
navController,
|
||||
onControlToggleRequested,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,8 +84,27 @@ fun NavGraphBuilder.screen(screen: Screen, navController: NavController) {
|
||||
private fun ScreenMenu(
|
||||
screen: ParentScreen,
|
||||
navController: NavController,
|
||||
onControlToggleRequested: () -> Unit,
|
||||
) {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
LazyColumn(
|
||||
Modifier.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
item {
|
||||
Surface(
|
||||
Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
shape = CircleShape,
|
||||
) {
|
||||
Column(
|
||||
Modifier.clickable(onClick = onControlToggleRequested).padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text("Toggle controls")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screen.children.forEach { (name, child) ->
|
||||
item {
|
||||
Surface(
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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.people
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.core.graphics.drawable.toIcon
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.people.data.model.PeopleTileModel
|
||||
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
||||
import com.android.systemui.people.widget.PeopleTileKey
|
||||
|
||||
/** A [PeopleViewModel] that does not have any conversations. */
|
||||
fun emptyPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
|
||||
return fakePeopleSpaceViewModel(context, emptyList(), emptyList())
|
||||
}
|
||||
|
||||
/** A [PeopleViewModel] that has a few conversations. */
|
||||
fun fewPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
|
||||
return fakePeopleSpaceViewModel(
|
||||
context,
|
||||
priorityTiles =
|
||||
listOf(
|
||||
fakeTile(context, id = "0", Color.RED, "Priority"),
|
||||
fakeTile(context, id = "1", Color.BLUE, "Priority NewStory", hasNewStory = true),
|
||||
),
|
||||
recentTiles =
|
||||
listOf(
|
||||
fakeTile(context, id = "2", Color.GREEN, "Recent Important", isImportant = true),
|
||||
fakeTile(context, id = "3", Color.CYAN, "Recent DndBlocking", isDndBlocking = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/** A [PeopleViewModel] that has a lot of conversations. */
|
||||
fun fullPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
|
||||
return fakePeopleSpaceViewModel(
|
||||
context,
|
||||
priorityTiles =
|
||||
listOf(
|
||||
fakeTile(context, id = "0", Color.RED, "Priority"),
|
||||
fakeTile(context, id = "1", Color.BLUE, "Priority NewStory", hasNewStory = true),
|
||||
fakeTile(context, id = "2", Color.GREEN, "Priority Important", isImportant = true),
|
||||
fakeTile(
|
||||
context,
|
||||
id = "3",
|
||||
Color.CYAN,
|
||||
"Priority DndBlocking",
|
||||
isDndBlocking = true,
|
||||
),
|
||||
fakeTile(
|
||||
context,
|
||||
id = "4",
|
||||
Color.MAGENTA,
|
||||
"Priority NewStory Important",
|
||||
hasNewStory = true,
|
||||
isImportant = true,
|
||||
),
|
||||
),
|
||||
recentTiles =
|
||||
listOf(
|
||||
fakeTile(
|
||||
context,
|
||||
id = "5",
|
||||
Color.RED,
|
||||
"Recent NewStory DndBlocking",
|
||||
hasNewStory = true,
|
||||
isDndBlocking = true,
|
||||
),
|
||||
fakeTile(
|
||||
context,
|
||||
id = "6",
|
||||
Color.BLUE,
|
||||
"Recent Important DndBlocking",
|
||||
isImportant = true,
|
||||
isDndBlocking = true,
|
||||
),
|
||||
fakeTile(
|
||||
context,
|
||||
id = "7",
|
||||
Color.GREEN,
|
||||
"Recent NewStory Important DndBlocking",
|
||||
hasNewStory = true,
|
||||
isImportant = true,
|
||||
isDndBlocking = true,
|
||||
),
|
||||
fakeTile(context, id = "8", Color.CYAN, "Recent"),
|
||||
fakeTile(context, id = "9", Color.MAGENTA, "Recent"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun fakePeopleSpaceViewModel(
|
||||
@Application context: Context,
|
||||
priorityTiles: List<PeopleTileModel>,
|
||||
recentTiles: List<PeopleTileModel>,
|
||||
): PeopleViewModel {
|
||||
return PeopleViewModel(
|
||||
context,
|
||||
FakePeopleTileRepository(priorityTiles, recentTiles),
|
||||
FakePeopleWidgetRepository(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun fakeTile(
|
||||
@Application context: Context,
|
||||
id: String,
|
||||
iconColor: Int,
|
||||
username: String,
|
||||
hasNewStory: Boolean = false,
|
||||
isImportant: Boolean = false,
|
||||
isDndBlocking: Boolean = false
|
||||
): PeopleTileModel {
|
||||
return PeopleTileModel(
|
||||
PeopleTileKey(id, /* userId= */ 0, /* packageName */ ""),
|
||||
username,
|
||||
fakeUserIcon(context, iconColor),
|
||||
hasNewStory,
|
||||
isImportant,
|
||||
isDndBlocking,
|
||||
)
|
||||
}
|
||||
|
||||
private fun fakeUserIcon(@Application context: Context, color: Int): Icon {
|
||||
val size = context.resources.getDimensionPixelSize(R.dimen.avatar_size_for_medium)
|
||||
val bitmap =
|
||||
Bitmap.createBitmap(
|
||||
size,
|
||||
size,
|
||||
Bitmap.Config.ARGB_8888,
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint().apply { this.color = color }
|
||||
val radius = size / 2f
|
||||
canvas.drawCircle(/* cx= */ radius, /* cy= */ radius, /* radius= */ radius, paint)
|
||||
return bitmap.toIcon()
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import com.android.systemui.people.data.model.PeopleTileModel
|
||||
import com.android.systemui.people.data.repository.PeopleTileRepository
|
||||
import com.android.systemui.people.data.repository.PeopleWidgetRepository
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -52,7 +51,7 @@ class PeopleViewModel(
|
||||
* reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
|
||||
*/
|
||||
private val _priorityTiles = MutableStateFlow(priorityTiles())
|
||||
val priorityTiles: Flow<List<PeopleTileViewModel>> = _priorityTiles.asStateFlow()
|
||||
val priorityTiles: StateFlow<List<PeopleTileViewModel>> = _priorityTiles.asStateFlow()
|
||||
|
||||
/**
|
||||
* The list of the priority tiles/conversations.
|
||||
@@ -61,7 +60,7 @@ class PeopleViewModel(
|
||||
* reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
|
||||
*/
|
||||
private val _recentTiles = MutableStateFlow(recentTiles())
|
||||
val recentTiles: Flow<List<PeopleTileViewModel>> = _recentTiles.asStateFlow()
|
||||
val recentTiles: StateFlow<List<PeopleTileViewModel>> = _recentTiles.asStateFlow()
|
||||
|
||||
/** The ID of the widget currently being edited/added. */
|
||||
private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)
|
||||
|
||||
Reference in New Issue
Block a user