Merge "[expressive design] Migrate AppList Page." into main
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPageProvider.kt
index 4d3a78a5..f2bc380 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPageProvider.kt
@@ -17,8 +17,12 @@
package com.android.settingslib.spa.gallery.ui
import android.os.Bundle
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPageProvider
@@ -30,6 +34,7 @@
import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spa.widget.ui.Category
+import com.android.settingslib.spa.widget.ui.LazyCategory
private const val TITLE = "Sample Category"
@@ -65,7 +70,7 @@
)
entryList.add(
SettingsEntryBuilder.create("Preference 3", owner)
- .setMacro { SimplePreferenceMacro(title = "Preference 2", summary = "Summary 3") }
+ .setMacro { SimplePreferenceMacro(title = "Preference 3", summary = "Summary 3") }
.build()
)
entryList.add(
@@ -88,6 +93,13 @@
entries[2].UiLayout()
entries[3].UiLayout()
}
+ Column(Modifier.height(200.dp)) {
+ LazyCategory(
+ list = entries,
+ entry = { index: Int -> @Composable { entries[index].UiLayout() } },
+ title = { index: Int -> if (index == 0 || index == 2) "LazyCategory" else null },
+ ) {}
+ }
}
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
index 66680fa..28b2b4a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
@@ -19,8 +19,13 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.TouchApp
import androidx.compose.material3.MaterialTheme
@@ -34,6 +39,7 @@
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.framework.theme.SettingsShape
@@ -98,6 +104,57 @@
}
}
+/**
+ * A container that is used to group items with lazy loading.
+ *
+ * @param list The list of items to display.
+ * @param entry The entry for each list item according to its index in list.
+ * @param key Optional. The key for each item in list to provide unique item identifiers, making
+ * the list more efficient.
+ * @param title Optional. Category title for each item or each group of items in the list. It
+ * should be decided by the index.
+ * @param bottomPadding Optional. Bottom outside padding of the category.
+ * @param state Optional. State of LazyList.
+ * @param content Optional. Content to be shown at the top of the category.
+ */
+
+@Composable
+fun LazyCategory(
+ list: List<Any>,
+ entry: (Int) -> @Composable () -> Unit,
+ key: ((Int) -> Any)? = null,
+ title: ((Int) -> String?)? = null,
+ bottomPadding: Dp = SettingsDimension.paddingSmall,
+ state: LazyListState = rememberLazyListState(),
+ content: @Composable () -> Unit,
+) {
+ Column(
+ Modifier.padding(
+ PaddingValues(
+ start = SettingsDimension.paddingLarge,
+ end = SettingsDimension.paddingLarge,
+ top = SettingsDimension.paddingSmall,
+ bottom = bottomPadding,
+ )
+ )
+ .clip(SettingsShape.CornerMedium2)
+ ) {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(SettingsDimension.paddingTiny),
+ state = state,
+ ) {
+ item { content() }
+
+ items(count = list.size, key = key) {
+ title?.invoke(it)?.let { title -> CategoryTitle(title) }
+ val entryPreference = entry(it)
+ entryPreference()
+ }
+ }
+ }
+}
+
@Preview
@Composable
private fun CategoryPreview() {
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt
index 09a6e6d..4b4a8c2 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt
@@ -16,10 +16,16 @@
package com.android.settingslib.spa.widget.ui
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
@@ -30,14 +36,11 @@
@RunWith(AndroidJUnit4::class)
class CategoryTest {
- @get:Rule
- val composeTestRule = createComposeRule()
+ @get:Rule val composeTestRule = createComposeRule()
@Test
fun categoryTitle() {
- composeTestRule.setContent {
- CategoryTitle(title = "CategoryTitle")
- }
+ composeTestRule.setContent { CategoryTitle(title = "CategoryTitle") }
composeTestRule.onNodeWithText("CategoryTitle").assertIsDisplayed()
}
@@ -46,12 +49,14 @@
fun category_hasContent_titleDisplayed() {
composeTestRule.setContent {
Category(title = "CategoryTitle") {
- Preference(remember {
- object : PreferenceModel {
- override val title = "Some Preference"
- override val summary = { "Some summary" }
+ Preference(
+ remember {
+ object : PreferenceModel {
+ override val title = "Some Preference"
+ override val summary = { "Some summary" }
+ }
}
- })
+ )
}
}
@@ -60,10 +65,45 @@
@Test
fun category_noContent_titleNotDisplayed() {
- composeTestRule.setContent {
- Category(title = "CategoryTitle") {}
- }
+ composeTestRule.setContent { Category(title = "CategoryTitle") {} }
composeTestRule.onNodeWithText("CategoryTitle").assertDoesNotExist()
}
+
+ @Test
+ fun lazyCategory_content_displayed() {
+ composeTestRule.setContent { TestLazyCategory() }
+
+ composeTestRule.onNodeWithText("text").assertExists()
+ }
+
+ @Test
+ fun lazyCategory_title_displayed() {
+ composeTestRule.setContent { TestLazyCategory() }
+
+ composeTestRule.onNodeWithText("LazyCategory 0").assertExists()
+ composeTestRule.onNodeWithText("LazyCategory 1").assertDoesNotExist()
+ }
+}
+
+@Composable
+private fun TestLazyCategory() {
+ val list: List<PreferenceModel> =
+ listOf(
+ object : PreferenceModel {
+ override val title = "title"
+ },
+ object : PreferenceModel {
+ override val title = "title"
+ },
+ )
+ Column(Modifier.height(200.dp)) {
+ LazyCategory(
+ list = list,
+ entry = { index: Int -> @Composable { Preference(list[index]) } },
+ title = { index: Int -> if (index == 0) "LazyCategory $index" else null },
+ ) {
+ Text("text")
+ }
+ }
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
index bededf0..2a214b6 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
@@ -37,7 +37,9 @@
import com.android.settingslib.spa.framework.compose.LogCompositions
import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer
import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll
+import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
import com.android.settingslib.spa.widget.ui.CategoryTitle
+import com.android.settingslib.spa.widget.ui.LazyCategory
import com.android.settingslib.spa.widget.ui.PlaceholderTitle
import com.android.settingslib.spa.widget.ui.Spinner
import com.android.settingslib.spa.widget.ui.SpinnerOption
@@ -55,19 +57,14 @@
private const val TAG = "AppList"
private const val CONTENT_TYPE_HEADER = "header"
-/**
- * The config used to load the App List.
- */
+/** The config used to load the App List. */
data class AppListConfig(
val userIds: List<Int>,
val showInstantApps: Boolean,
val matchAnyUserForAdmin: Boolean,
)
-data class AppListState(
- val showSystem: () -> Boolean,
- val searchQuery: () -> String,
-)
+data class AppListState(val showSystem: () -> Boolean, val searchQuery: () -> String)
data class AppListInput<T : AppRecord>(
val config: AppListConfig,
@@ -90,7 +87,7 @@
@Composable
internal fun <T : AppRecord> AppListInput<T>.AppListImpl(
- viewModelSupplier: @Composable () -> IAppListViewModel<T>,
+ viewModelSupplier: @Composable () -> IAppListViewModel<T>
) {
LogCompositions(TAG, config.userIds.toString())
val viewModel = viewModelSupplier()
@@ -125,7 +122,7 @@
appListData: State<AppListData<T>?>,
header: @Composable () -> Unit,
bottomPadding: Dp,
- noItemMessage: String?
+ noItemMessage: String?,
) {
val timeMeasurer = rememberTimeMeasurer(TAG)
appListData.value?.let { (list, option) ->
@@ -135,40 +132,61 @@
PlaceholderTitle(noItemMessage ?: stringResource(R.string.no_applications))
return
}
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- state = rememberLazyListStateAndHideKeyboardWhenStartScroll(),
- contentPadding = PaddingValues(bottom = bottomPadding),
- ) {
- item(contentType = CONTENT_TYPE_HEADER) {
+ if (isSpaExpressiveEnabled) {
+ LazyCategory(
+ list = list,
+ entry = { index: Int ->
+ @Composable {
+ val appEntry = list[index]
+ val summary = getSummary(option, appEntry.record) ?: { "" }
+ remember(appEntry) {
+ AppListItemModel(appEntry.record, appEntry.label, summary)
+ }
+ .AppItem()
+ }
+ },
+ key = { index: Int -> list[index].record.itemKey(option) },
+ title = { index: Int -> getGroupTitle(option, list[index].record) },
+ bottomPadding = bottomPadding,
+ state = rememberLazyListStateAndHideKeyboardWhenStartScroll(),
+ ) {
header()
}
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = rememberLazyListStateAndHideKeyboardWhenStartScroll(),
+ contentPadding = PaddingValues(bottom = bottomPadding),
+ ) {
+ item(contentType = CONTENT_TYPE_HEADER) { header() }
- items(count = list.size, key = { list[it].record.itemKey(option) }) {
- remember(list) { getGroupTitleIfFirst(option, list, it) }
- ?.let { group -> CategoryTitle(title = group) }
+ items(count = list.size, key = { list[it].record.itemKey(option) }) {
+ remember(list) { getGroupTitleIfFirst(option, list, it) }
+ ?.let { group -> CategoryTitle(title = group) }
- val appEntry = list[it]
- val summary = getSummary(option, appEntry.record) ?: { "" }
- remember(appEntry) {
- AppListItemModel(appEntry.record, appEntry.label, summary)
- }.AppItem()
+ val appEntry = list[it]
+ val summary = getSummary(option, appEntry.record) ?: { "" }
+ remember(appEntry) {
+ AppListItemModel(appEntry.record, appEntry.label, summary)
+ }
+ .AppItem()
+ }
}
}
}
}
-private fun <T : AppRecord> T.itemKey(option: Int) =
- listOf(option, app.packageName, app.userId)
+private fun <T : AppRecord> T.itemKey(option: Int) = listOf(option, app.packageName, app.userId)
/** Returns group title if this is the first item of the group. */
private fun <T : AppRecord> AppListModel<T>.getGroupTitleIfFirst(
option: Int,
list: List<AppEntry<T>>,
index: Int,
-): String? = getGroupTitle(option, list[index].record)?.takeIf {
- index == 0 || it != getGroupTitle(option, list[index - 1].record)
-}
+): String? =
+ getGroupTitle(option, list[index].record)?.takeIf {
+ index == 0 || it != getGroupTitle(option, list[index - 1].record)
+ }
@Composable
private fun <T : AppRecord> rememberViewModel(
@@ -183,16 +201,19 @@
viewModel.searchQuery.Sync(state.searchQuery)
LifecycleEffect(onStart = { viewModel.reloadApps() })
- val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
- addAction(Intent.ACTION_PACKAGE_REMOVED)
- addAction(Intent.ACTION_PACKAGE_CHANGED)
- addDataScheme("package")
- }
+ val intentFilter =
+ IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
+ addAction(Intent.ACTION_PACKAGE_REMOVED)
+ addAction(Intent.ACTION_PACKAGE_CHANGED)
+ addDataScheme("package")
+ }
for (userId in config.userIds) {
DisposableBroadcastReceiverAsUser(
intentFilter = intentFilter,
userHandle = UserHandle.of(userId),
- ) { viewModel.reloadApps() }
+ ) {
+ viewModel.reloadApps()
+ }
}
return viewModel
}