diff options
4 files changed, 180 insertions, 50 deletions
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 4d3a78a583fc..f2bc380a93de 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.PreferenceModel 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 @@ object CategoryPageProvider : SettingsPageProvider { ) 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 @@ object CategoryPageProvider : SettingsPageProvider { 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 66680fa547b1..28b2b4ab1662 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 @@ package com.android.settingslib.spa.widget.ui 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.Modifier 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 @@ fun Category(title: String? = null, content: @Composable ColumnScope.() -> Unit) } } +/** + * 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 09a6e6ddc7f0..4b4a8c20b39e 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 @@ import org.junit.runner.RunWith @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 @@ class CategoryTest { 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 @@ class CategoryTest { @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 bededf03a0f4..2a214b6f74a6 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.LifecycleEffect 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 @@ import kotlinx.coroutines.flow.MutableStateFlow 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 @@ fun <T : AppRecord> AppListInput<T>.AppList() { @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 @@ private fun <T : AppRecord> AppListModel<T>.AppListWidget( 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 @@ private fun <T : AppRecord> AppListModel<T>.AppListWidget( 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() } - - 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() + } 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) } + + 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 @@ private fun <T : AppRecord> rememberViewModel( 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 } |