Merge "Implement the People UI in Compose (1/2)" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
ea08a5217b
@@ -30,6 +30,7 @@ android_library {
|
|||||||
],
|
],
|
||||||
|
|
||||||
static_libs: [
|
static_libs: [
|
||||||
|
"SystemUI-core",
|
||||||
"SystemUIComposeCore",
|
"SystemUIComposeCore",
|
||||||
|
|
||||||
"androidx.compose.runtime_runtime",
|
"androidx.compose.runtime_runtime",
|
||||||
|
|||||||
@@ -16,7 +16,38 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
|
||||||
package="com.android.systemui.compose.features">
|
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>
|
</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: [
|
srcs: [
|
||||||
"src/**/*.kt",
|
"src/**/*.kt",
|
||||||
|
":SystemUI-tests-utils",
|
||||||
],
|
],
|
||||||
|
|
||||||
resource_dirs: [
|
resource_dirs: [
|
||||||
@@ -45,6 +46,14 @@ android_library {
|
|||||||
"androidx.navigation_navigation-compose",
|
"androidx.navigation_navigation-compose",
|
||||||
|
|
||||||
"androidx.appcompat_appcompat",
|
"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"],
|
kotlincflags: ["-Xjvm-default=all"],
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -33,6 +33,28 @@ object GalleryAppScreens {
|
|||||||
val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
|
val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
|
||||||
val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() }
|
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 =
|
val Home =
|
||||||
ParentScreen(
|
ParentScreen(
|
||||||
"home",
|
"home",
|
||||||
@@ -41,20 +63,21 @@ object GalleryAppScreens {
|
|||||||
"Material colors" to MaterialColors,
|
"Material colors" to MaterialColors,
|
||||||
"Android colors" to AndroidColors,
|
"Android colors" to AndroidColors,
|
||||||
"Example feature" to ExampleFeature,
|
"Example feature" to ExampleFeature,
|
||||||
|
"People" to People,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The main content of the app, that shows [GalleryAppScreens.Home] by default. */
|
/** The main content of the app, that shows [GalleryAppScreens.Home] by default. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun MainContent() {
|
private fun MainContent(onControlToggleRequested: () -> Unit) {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = GalleryAppScreens.Home.identifier,
|
startDestination = GalleryAppScreens.Home.identifier,
|
||||||
) {
|
) {
|
||||||
screen(GalleryAppScreens.Home, navController)
|
screen(GalleryAppScreens.Home, navController, onControlToggleRequested)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +92,7 @@ fun GalleryApp(
|
|||||||
onChangeTheme: () -> Unit,
|
onChangeTheme: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val systemFontScale = LocalDensity.current.fontScale
|
val systemFontScale = LocalDensity.current.fontScale
|
||||||
var fontScale: FontScale by remember {
|
var fontScale: FontScale by rememberSaveable {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
FontScale.values().firstOrNull { it.scale == systemFontScale } ?: FontScale.Normal
|
FontScale.values().firstOrNull { it.scale == systemFontScale } ?: FontScale.Normal
|
||||||
)
|
)
|
||||||
@@ -87,7 +110,7 @@ fun GalleryApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val systemLayoutDirection = LocalLayoutDirection.current
|
val systemLayoutDirection = LocalLayoutDirection.current
|
||||||
var layoutDirection by remember { mutableStateOf(systemLayoutDirection) }
|
var layoutDirection by rememberSaveable { mutableStateOf(systemLayoutDirection) }
|
||||||
val onChangeLayoutDirection = {
|
val onChangeLayoutDirection = {
|
||||||
layoutDirection =
|
layoutDirection =
|
||||||
when (layoutDirection) {
|
when (layoutDirection) {
|
||||||
@@ -105,19 +128,24 @@ fun GalleryApp(
|
|||||||
Modifier.fillMaxSize(),
|
Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
) {
|
) {
|
||||||
Column(Modifier.fillMaxSize().systemBarsPadding().padding(16.dp)) {
|
Column(Modifier.fillMaxSize().systemBarsPadding()) {
|
||||||
ConfigurationControls(
|
var showControls by rememberSaveable { mutableStateOf(true) }
|
||||||
theme,
|
|
||||||
fontScale,
|
|
||||||
layoutDirection,
|
|
||||||
onChangeTheme,
|
|
||||||
onChangeLayoutDirection,
|
|
||||||
onChangeFontScale,
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
) : Screen(identifier)
|
||||||
|
|
||||||
/** Create the navigation graph for [screen]. */
|
/** Create the navigation graph for [screen]. */
|
||||||
fun NavGraphBuilder.screen(screen: Screen, navController: NavController) {
|
fun NavGraphBuilder.screen(
|
||||||
|
screen: Screen,
|
||||||
|
navController: NavController,
|
||||||
|
onControlToggleRequested: () -> Unit,
|
||||||
|
) {
|
||||||
when (screen) {
|
when (screen) {
|
||||||
is ChildScreen -> composable(screen.identifier) { screen.content(navController) }
|
is ChildScreen -> composable(screen.identifier) { screen.content(navController) }
|
||||||
is ParentScreen -> {
|
is ParentScreen -> {
|
||||||
val menuRoute = "${screen.identifier}_menu"
|
val menuRoute = "${screen.identifier}_menu"
|
||||||
navigation(startDestination = menuRoute, route = screen.identifier) {
|
navigation(startDestination = menuRoute, route = screen.identifier) {
|
||||||
// The menu to navigate to one of the children screens.
|
// 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.
|
// 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(
|
private fun ScreenMenu(
|
||||||
screen: ParentScreen,
|
screen: ParentScreen,
|
||||||
navController: NavController,
|
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) ->
|
screen.children.forEach { (name, child) ->
|
||||||
item {
|
item {
|
||||||
Surface(
|
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.PeopleTileRepository
|
||||||
import com.android.systemui.people.data.repository.PeopleWidgetRepository
|
import com.android.systemui.people.data.repository.PeopleWidgetRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -52,7 +51,7 @@ class PeopleViewModel(
|
|||||||
* reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
|
* reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
|
||||||
*/
|
*/
|
||||||
private val _priorityTiles = MutableStateFlow(priorityTiles())
|
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.
|
* 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.
|
* reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
|
||||||
*/
|
*/
|
||||||
private val _recentTiles = MutableStateFlow(recentTiles())
|
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. */
|
/** The ID of the widget currently being edited/added. */
|
||||||
private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)
|
private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)
|
||||||
|
|||||||
Reference in New Issue
Block a user