Replace UserView with StackViews
Bug: 426102764
Test: Manual
Flag: android.multiuser.widget_current_user_view
Change-Id: Ia62b51abf02a943c313ce3275ead7ae0ee072409
diff --git a/Widget/src/main/java/com/android/multiuser/widget/ui/view/AdaptiveUserView.kt b/Widget/src/main/java/com/android/multiuser/widget/ui/view/AdaptiveUserView.kt
deleted file mode 100644
index 6b45648..0000000
--- a/Widget/src/main/java/com/android/multiuser/widget/ui/view/AdaptiveUserView.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2025 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.multiuser.widget.ui.view
-
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.unit.sp
-import androidx.glance.GlanceModifier
-import androidx.glance.Image
-import androidx.glance.ImageProvider
-import androidx.glance.layout.size
-import androidx.glance.text.Text
-import androidx.glance.text.TextStyle
-import com.android.multiuser.widget.R
-import com.android.multiuser.widget.ui.view.layout.AdaptivePane
-import com.android.multiuser.widget.viewmodel.UiState
-import com.android.multiuser.widget.viewmodel.UserStack
-import com.android.multiuser.widget.viewmodel.UserViewModel
-import kotlinx.coroutines.launch
-
-/**
- * Displays user data in the widget.
- */
-@Composable
-fun AdaptiveUserView (
- viewModel: UserViewModel,
- userStack: UserStack,
- modifier: GlanceModifier,
-) {
- val bitmap by viewModel.bitmap.collectAsState()
- val scope = rememberCoroutineScope()
- val uiState by viewModel.uiState.collectAsState()
- scope.launch {
- if (uiState != UiState.Loading) {
- viewModel.loadAvatar()
- }
- }
- AdaptivePane(modifier = modifier,
- arrangement = userStack.arrangement,
- startPane = {
- Image(
- provider = bitmap?.let { ImageProvider(it) }
- ?: ImageProvider(R.drawable.account_circle),
- contentDescription = viewModel.contentDescription,
- modifier = GlanceModifier.size(userStack.imageSize)
- )},
- endPane = { if (userStack.textMetric.fontSize > 0.sp) {
- Text(
- text = viewModel.name,
- maxLines = 1,
- style = TextStyle(fontSize = userStack.textMetric.fontSize),
- )
- } else { null }
- })
-}
\ No newline at end of file
diff --git a/Widget/src/main/java/com/android/multiuser/widget/ui/view/LayoutView.kt b/Widget/src/main/java/com/android/multiuser/widget/ui/view/LayoutView.kt
index 8e25023..4b9ca8c 100644
--- a/Widget/src/main/java/com/android/multiuser/widget/ui/view/LayoutView.kt
+++ b/Widget/src/main/java/com/android/multiuser/widget/ui/view/LayoutView.kt
@@ -70,7 +70,7 @@
arrangement = layout.arrangement,
startPane = {
layout.userStack?.let { userStack ->
- AdaptiveUserView(
+ UserStackView(
viewModel = userViewModel,
userStack,
modifier = GlanceModifier
diff --git a/Widget/src/main/java/com/android/multiuser/widget/ui/view/UserStackView.kt b/Widget/src/main/java/com/android/multiuser/widget/ui/view/UserStackView.kt
new file mode 100644
index 0000000..5d0c6b8
--- /dev/null
+++ b/Widget/src/main/java/com/android/multiuser/widget/ui/view/UserStackView.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2025 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.multiuser.widget.ui.view
+
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.glance.GlanceModifier
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import com.android.multiuser.stacks.Stack
+import com.android.multiuser.stacks.ui.StackView
+import com.android.multiuser.stacks.ui.size
+import com.android.multiuser.widget.R
+import com.android.multiuser.widget.viewmodel.UiState
+import com.android.multiuser.widget.viewmodel.stacks.UserStacks
+import com.android.multiuser.widget.viewmodel.UserViewModel
+import com.android.multiuser.widget.viewmodel.measurables.TextMeasurable
+import kotlinx.coroutines.launch
+
+/**
+ * Displays user data in the widget.
+ */
+@Composable
+fun UserStackView (
+ viewModel: UserViewModel,
+ stack: Stack,
+ modifier: GlanceModifier,
+) {
+ val bitmap by viewModel.bitmap.collectAsState()
+ val scope = rememberCoroutineScope()
+ val uiState by viewModel.uiState.collectAsState()
+ scope.launch {
+ if (uiState != UiState.Loading) {
+ viewModel.loadAvatar()
+ }
+ }
+ StackView(
+ items = stack.list.mapNotNull { measurable ->
+ // Create a composable lambda for each item by wrapping in {}
+ when (measurable) {
+ is UserStacks.UserImageSquare -> {
+ // Return a lambda: @Composable () -> Unit
+ {
+ Image(
+ provider = bitmap?.let { ImageProvider(it) }
+ ?: ImageProvider(R.drawable.account_circle),
+ contentDescription = viewModel.contentDescription,
+ modifier = GlanceModifier.size(measurable.vertically.min)
+ )
+ }
+ }
+ is TextMeasurable -> {
+ // Return another lambda: @Composable () -> Unit
+ {
+ Text(
+ text = measurable.text,
+ maxLines = 1,
+ style = TextStyle(fontSize = measurable.fontSize),
+ )
+ }
+ }
+ // mapNotNull will automatically filter out the nulls
+ else -> null
+ }
+ },
+ stack = stack,
+ modifier = modifier
+ )
+}
\ No newline at end of file
diff --git a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/LayoutViewModel.kt b/Widget/src/main/java/com/android/multiuser/widget/viewmodel/LayoutViewModel.kt
index 476271f..cba34eb 100644
--- a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/LayoutViewModel.kt
+++ b/Widget/src/main/java/com/android/multiuser/widget/viewmodel/LayoutViewModel.kt
@@ -21,9 +21,11 @@
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import com.android.multiuser.widget.R
+import com.android.multiuser.widget.viewmodel.stacks.ActionStacks
+import com.android.multiuser.stacks.Stack
import com.android.multiuser.stacks.ui.dp
import com.android.multiuser.stacks.ui.inDp
-import com.android.multiuser.widget.viewmodel.stacks.ActionStacks
+import com.android.multiuser.widget.viewmodel.stacks.UserStacks
class LayoutViewModel(
var size: DpSize, val res: Resources, val userViewModel: UserViewModel,
@@ -35,12 +37,12 @@
} else {
Arrangement.Horizontal()
}
- var userStack: UserStack? = null
+ var userStack: Stack? = null
val widgetPadding = res.dp(R.dimen.content_padding) * 2// top+bottom or left+right
// with title bar visible we do not set top padding
val titleBarHeight = res.dp(R.dimen.title_bar_height) - res.dp(R.dimen.content_padding)
- val userStacks = UserStacks(res, userViewModel.name)
+ val userStacks = UserStacks(res, userViewModel)
// TODO: replace with more stacks when user stacks are measurable
val actionStack = ActionStacks(res, actionViewModelList).TALL_HORIZONTAL
@@ -59,8 +61,8 @@
listOf(
userStacks.SMALL_VERTICAL,
userStacks.LARGE_VERTICAL
- ).filter { it.height + verticalHeight + titleBarHeight <= (size.height) }
- .maxByOrNull { it.height }
+ ).filter { it.vertically.min.inDp(res.displayMetrics) + verticalHeight + titleBarHeight <= (size.height) }
+ .maxByOrNull { it.vertically.min.inDp(res.displayMetrics) }
if (userStack == null) { // title bar does not fit, check without title bar
(titleBarVisible as MutableStateFlow<Boolean>).value = false
// | 🙃 |
@@ -70,8 +72,8 @@
userStacks.SMALL_IMAGE_ONLY,
userStacks.SMALL_VERTICAL,
userStacks.LARGE_VERTICAL
- ).filter { it.height + verticalHeight <= (size.height) }
- .maxByOrNull { it.height }
+ ).filter { it.vertically.min.inDp(res.displayMetrics) + verticalHeight <= (size.height) }
+ .maxByOrNull { it.vertically.min.inDp(res.displayMetrics) }
if (userStack == null) { // user name does not fit, use small image only
// | 🔄 ➕ |
}
@@ -84,8 +86,8 @@
// | 🙃 | 🔄 |
// | Name | âž• |
var horizontalWidth = actionStackWidth + widgetPadding
- if (userStacks.LARGE_VERTICAL.height + titleBarHeight + widgetPadding <= (size.height)
- && (userStacks.LARGE_VERTICAL.width + horizontalWidth <= size.width)) {
+ if (userStacks.LARGE_VERTICAL.vertically.min.inDp(res.displayMetrics) + titleBarHeight + widgetPadding <= (size.height)
+ && (userStacks.LARGE_VERTICAL.horizontally.min.inDp(res.displayMetrics) + horizontalWidth <= size.width)) {
(titleBarVisible as MutableStateFlow<Boolean>).value = true
userStack = userStacks.LARGE_VERTICAL
} else { // Vertical layout does not fit, try all others
@@ -98,9 +100,9 @@
userStacks.SMALL_VERTICAL,
userStacks.SMALL_IMAGE_ONLY
).filter {
- (it.height + titleBarHeight + widgetPadding <= (size.height))
- && (it.width + horizontalWidth <= size.width)
- }.maxByOrNull { it.width }
+ (it.vertically.min.inDp(res.displayMetrics) + titleBarHeight + widgetPadding <= (size.height))
+ && (it.horizontally.min.inDp(res.displayMetrics) + horizontalWidth <= size.width)
+ }.maxByOrNull { it.horizontally.min.inDp(res.displayMetrics) }
(titleBarVisible as MutableStateFlow<Boolean>).value = true
if (userStack == null) {// Title bar does not fit, try without
// | 🙃 Name | 🔄 ➕ | | 🙃 | 🔄 ➕ |
@@ -109,9 +111,9 @@
userStacks.LARGE_HORIZONTAL,
userStacks.SMALL_IMAGE_ONLY
).filter {
- (it.height + widgetPadding <= (size.height))
- && (it.width + horizontalWidth <= size.width)
- }.maxByOrNull { it.width }
+ (it.vertically.min.inDp(res.displayMetrics) + widgetPadding <= (size.height))
+ && (it.horizontally.min.inDp(res.displayMetrics) + horizontalWidth <= size.width)
+ }.maxByOrNull { it.horizontally.min.inDp(res.displayMetrics) }
(titleBarVisible as MutableStateFlow<Boolean>).value = false
}
if (userStack == null) {
diff --git a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/UserStack.kt b/Widget/src/main/java/com/android/multiuser/widget/viewmodel/UserStack.kt
deleted file mode 100644
index dc0a58e..0000000
--- a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/UserStack.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2025 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.multiuser.widget.viewmodel
-
-import androidx.compose.ui.unit.Dp
-import com.android.multiuser.widget.viewmodel.measurables.TextMeasurable
-
-/**
- * Single User Stack = person image + person name
- */
-class UserStack(val arrangement: Arrangement, val imageSize: Dp, val textMetric: TextMeasurable) {
- val width = if(arrangement is Arrangement.Horizontal) {
- imageSize + textMetric.width + arrangement.gutter + arrangement.padding
- } else {
- imageSize + arrangement.padding
- }
-
- val height = if(arrangement is Arrangement.Vertical) {
- imageSize + textMetric.height + arrangement.gutter + arrangement.padding
- } else {
- imageSize + arrangement.padding
- }
-
- override fun toString(): String {
- return if(arrangement is Arrangement.Vertical) {"V"} else {"H"} + " ${width} x ${height} imageSize: $imageSize gutter: ${arrangement.gutter}\ntext: ${textMetric.width}x${textMetric.height}"
- }
-}
\ No newline at end of file
diff --git a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/UserStacks.kt b/Widget/src/main/java/com/android/multiuser/widget/viewmodel/UserStacks.kt
deleted file mode 100644
index b200830..0000000
--- a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/UserStacks.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2025 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.multiuser.widget.viewmodel
-
-import android.content.res.Resources
-import androidx.compose.ui.unit.Dp
-import com.android.multiuser.widget.R
-import com.android.multiuser.stacks.ui.dp
-import com.android.multiuser.widget.viewmodel.measurables.TextMeasurable
-
-class UserStacks(val res: Resources,val text:String) {
- private val NONE_TEXT = TextMeasurable(res)
- private val MEDIUM_TEXT = TextMeasurable(res, R.dimen.user_text_min_size)
- private val LARGE_TEXT = TextMeasurable(res, R.dimen.user_text_max_size)
-
- val mediumImageSize: Dp = res.dp(R.dimen.user_image_min_size)
- val largeImageSize = res.dp(R.dimen.user_image_max_size)
- val verticalGutter = res.dp(R.dimen.user_vertical_gutter)
- val verticalArrangement = Arrangement.Vertical(verticalGutter)
- val horizontalGutter = res.dp(R.dimen.user_horizontal_gutter)
- val horizontalArrangement = Arrangement.Horizontal(horizontalGutter)
-
- val SMALL_IMAGE_ONLY =
- UserStack(Arrangement.Vertical(verticalGutter), mediumImageSize, NONE_TEXT)
- val SMALL_VERTICAL =
- UserStack(Arrangement.Vertical(verticalGutter), mediumImageSize, MEDIUM_TEXT)
- val LARGE_VERTICAL = UserStack(Arrangement.Vertical(verticalGutter), largeImageSize, LARGE_TEXT)
- val SMALL_HORIZONTAL =
- UserStack(Arrangement.Horizontal(horizontalGutter), mediumImageSize, MEDIUM_TEXT)
- val LARGE_HORIZONTAL =
- UserStack(Arrangement.Horizontal(horizontalGutter), largeImageSize, LARGE_TEXT)
-}
-
diff --git a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/stacks/UserStacks.kt b/Widget/src/main/java/com/android/multiuser/widget/viewmodel/stacks/UserStacks.kt
new file mode 100644
index 0000000..c270b99
--- /dev/null
+++ b/Widget/src/main/java/com/android/multiuser/widget/viewmodel/stacks/UserStacks.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2025 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.multiuser.widget.viewmodel.stacks
+
+import android.content.res.Resources
+import com.android.multiuser.stacks.FixedSquare
+import com.android.multiuser.stacks.Stack
+import com.android.multiuser.stacks.ui.px
+import com.android.multiuser.widget.R
+import com.android.multiuser.widget.viewmodel.UserViewModel
+import com.android.multiuser.widget.viewmodel.measurables.TextMeasurable
+
+class UserStacks(val res: Resources, val viewModel: UserViewModel) {
+ private val MEDIUM_TEXT = TextMeasurable(res, R.dimen.user_text_min_size, viewModel.name)
+ private val LARGE_TEXT = TextMeasurable(res, R.dimen.user_text_max_size, viewModel.name)
+
+ val mediumImageSize: Float = res.px(R.dimen.user_image_min_size)
+ val largeImageSize = res.px(R.dimen.user_image_max_size)
+ val verticalGutter = res.px(R.dimen.user_vertical_gutter)
+ val horizontalGutter = res.px(R.dimen.user_horizontal_gutter)
+
+ class UserImageSquare(val viewModel: UserViewModel, imageSize: Float) :FixedSquare (imageSize)
+
+ val mediumImageMeasurable = UserImageSquare(viewModel, mediumImageSize)
+ val largeImageMeasurable = UserImageSquare(viewModel, largeImageSize)
+
+ val SMALL_IMAGE_ONLY = Stack.Vertical(0f, list = listOf(mediumImageMeasurable))
+ .apply {
+ TAG = "USER Small Image Only"
+ horizontally.stretchToFill = true
+ }
+ val SMALL_VERTICAL = Stack.Vertical(verticalGutter, list = listOf(mediumImageMeasurable, MEDIUM_TEXT))
+ .apply {
+ horizontally.stretchToFill = true
+ TAG = "USER Small Vertical"
+ }
+ val LARGE_VERTICAL = Stack.Vertical(verticalGutter, list = listOf(largeImageMeasurable, LARGE_TEXT))
+ .apply {
+ TAG = "USER Large Vertical"
+ horizontally.stretchToFill = true
+ }
+ val SMALL_HORIZONTAL = Stack.Horizontal(horizontalGutter, list = listOf(mediumImageMeasurable, MEDIUM_TEXT)).apply { TAG = "USER Small Horizontal" }
+ val LARGE_HORIZONTAL = Stack.Horizontal(horizontalGutter, list = listOf(mediumImageMeasurable, LARGE_TEXT))
+ .apply {
+ TAG = "USER Large Horizontal"
+ horizontally.stretchToFill = true
+ vertically.stretchToFill = true
+ }
+}
+