From e663986c39f4047f5ad6483e51b3824c16123c3c Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Fri, 26 Aug 2022 07:32:13 +0800 Subject: [PATCH 1/3] Upgrade Compose version to 1.3 for SpaLib - Use Switch instead of Checkbox - Disable user scroll for Pager so, current scroll behavior is not supported. Bug: 235727273 Test: Unit test & Manual with Gallery App Change-Id: I0a5cbf011432c7c1f4545c249f7f75bb239a47c0 --- packages/SettingsLib/Spa/build.gradle | 3 +- .../spa/gallery/page/SettingsPagerPage.kt | 11 +++-- packages/SettingsLib/Spa/spa/build.gradle | 2 +- .../spa/framework/compose/Pager.kt | 3 +- .../spa/widget/scaffold/SettingsScaffold.kt | 6 +-- .../settingslib/spa/widget/ui/Switch.kt | 10 ++--- .../spaprivileged/template/app/AppListPage.kt | 41 +++++++++++-------- 7 files changed, 41 insertions(+), 35 deletions(-) diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle index 68c63dad67265..4fb77d7257caa 100644 --- a/packages/SettingsLib/Spa/build.gradle +++ b/packages/SettingsLib/Spa/build.gradle @@ -18,9 +18,8 @@ buildscript { ext { spa_min_sdk = 21 spa_target_sdk = 33 - jetpack_compose_version = '1.2.0-alpha04' + jetpack_compose_version = '1.3.0' jetpack_compose_compiler_version = '1.3.2' - jetpack_compose_material3_version = '1.0.0-alpha06' } } plugins { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt index e09ebdad7a1f0..b38178b0e6f4f 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt @@ -17,7 +17,10 @@ package com.android.settingslib.spa.gallery.page import android.os.Bundle +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPage @@ -48,9 +51,11 @@ object SettingsPagerPageProvider : SettingsPageProvider { @Composable override fun Page(arguments: Bundle?) { - SettingsScaffold(title = TITLE) { - SettingsPager(listOf("Personal", "Work")) { - PlaceholderTitle("Page $it") + SettingsScaffold(title = TITLE) { paddingValues -> + Box(Modifier.padding(paddingValues)) { + SettingsPager(listOf("Personal", "Work")) { + PlaceholderTitle("Page $it") + } } } } diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index 3bafcf27fed99..2820ed7bc96ea 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -55,7 +55,7 @@ android { dependencies { api "androidx.appcompat:appcompat:1.7.0-alpha01" - api "androidx.compose.material3:material3:$jetpack_compose_material3_version" + api "androidx.compose.material3:material3:1.1.0-alpha01" api "androidx.compose.material:material-icons-extended:$jetpack_compose_version" api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version" api "androidx.compose.ui:ui-tooling-preview:$jetpack_compose_version" diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt index bf338574c42d6..4df7794e87590 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt @@ -40,7 +40,6 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter @@ -214,6 +213,7 @@ internal fun Pager( horizontalAlignment = horizontalAlignment, reverseLayout = reverseLayout, contentPadding = contentPadding, + userScrollEnabled = false, modifier = modifier, ) { items( @@ -241,6 +241,7 @@ internal fun Pager( horizontalArrangement = Arrangement.spacedBy(itemSpacing, horizontalAlignment), reverseLayout = reverseLayout, contentPadding = contentPadding, + userScrollEnabled = false, modifier = modifier, ) { items( diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt index d17e4645ee5e2..b50cb3ed84d0a 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt @@ -21,8 +21,8 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold -import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -43,7 +43,7 @@ fun SettingsScaffold( ) { Scaffold( topBar = { - SmallTopAppBar( + TopAppBar( title = { Text( text = title, @@ -63,7 +63,7 @@ fun SettingsScaffold( @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun settingsTopAppBarColors() = TopAppBarDefaults.largeTopAppBarColors( +private fun settingsTopAppBarColors() = TopAppBarDefaults.smallTopAppBarColors( containerColor = SettingsTheme.colorScheme.surfaceHeader, scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader, ) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt index b9690762845e7..9831b918894b5 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt @@ -16,30 +16,26 @@ package com.android.settingslib.spa.widget.ui -import androidx.compose.material3.Checkbox -import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.State import com.android.settingslib.spa.framework.util.wrapOnSwitchWithLog -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsSwitch( checked: State, changeable: State, onCheckedChange: ((newChecked: Boolean) -> Unit)? = null, ) { - // TODO: Replace Checkbox with Switch when the androidx.compose.material3_material3 library is - // updated to date. val checkedValue = checked.value if (checkedValue != null) { - Checkbox( + Switch( checked = checkedValue, onCheckedChange = wrapOnSwitchWithLog(onCheckedChange), enabled = changeable.value, ) } else { - Checkbox( + Switch( checked = false, onCheckedChange = null, enabled = false, diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt index 2be1d1c6cce6b..0caa8213e1630 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt @@ -16,8 +16,8 @@ package com.android.settingslib.spaprivileged.template.app +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.DropdownMenuItem @@ -57,23 +57,28 @@ fun AppListPage( ShowSystemAction(showSystem.value) { showSystem.value = it } }, ) { paddingValues -> - Spacer(Modifier.padding(paddingValues)) - WorkProfilePager(primaryUserOnly) { userInfo -> - Column(Modifier.fillMaxSize()) { - val options = remember { listModel.getSpinnerOptions() } - val selectedOption = rememberSaveable { mutableStateOf(0) } - Spinner(options, selectedOption.value) { selectedOption.value = it } - AppList( - appListConfig = AppListConfig( - userId = userInfo.id, - showInstantApps = showInstantApps, - ), - listModel = listModel, - showSystem = showSystem, - option = selectedOption, - searchQuery = stateOf(""), - appItem = appItem, - ) + Box( + Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + WorkProfilePager(primaryUserOnly) { userInfo -> + Column(Modifier.fillMaxSize()) { + val options = remember { listModel.getSpinnerOptions() } + val selectedOption = rememberSaveable { mutableStateOf(0) } + Spinner(options, selectedOption.value) { selectedOption.value = it } + AppList( + appListConfig = AppListConfig( + userId = userInfo.id, + showInstantApps = showInstantApps, + ), + listModel = listModel, + showSystem = showSystem, + option = selectedOption, + searchQuery = stateOf(""), + appItem = appItem, + ) + } } } } From deda3d4737c0227da0608ee79cee7803b4a99619 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 25 Oct 2022 15:43:34 +0800 Subject: [PATCH 2/3] Add SearchScaffold for SpaLib This is used in App List to search the applications in list. Bug: 235727273 Test: Unit test & Manual with Settings App Change-Id: If75226f34b61c8c9fe311b38b135d2e91709c36c --- .../spa/framework/compose/Keyboards.kt | 57 ++++++ .../spa/framework/theme/SettingsOpacity.kt | 1 + .../spa/widget/scaffold/Actions.kt | 41 +++- .../spa/widget/scaffold/SearchScaffold.kt | 189 ++++++++++++++++++ .../spa/widget/scaffold/SettingsScaffold.kt | 30 +-- .../spa/widget/scaffold/SettingsTopAppBar.kt | 57 ++++++ .../spa/framework => }/OverridableFlowTest.kt | 3 +- .../spa/widget/scaffold/SearchScaffoldTest.kt | 146 ++++++++++++++ .../spaprivileged/template/app/AppList.kt | 4 +- .../spaprivileged/template/app/AppListPage.kt | 48 ++--- 10 files changed, 508 insertions(+), 68 deletions(-) create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt rename packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/{com/android/settingslib/spa/framework => }/OverridableFlowTest.kt (92%) create mode 100644 packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt new file mode 100644 index 0000000000000..8d0313fdce9f3 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt @@ -0,0 +1,57 @@ +/* + * 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.settingslib.spa.framework.compose + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.KeyboardActionScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter + +/** + * An action when run, hides the keyboard if it's open. + */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun hideKeyboardAction(): KeyboardActionScope.() -> Unit { + val keyboardController = LocalSoftwareKeyboardController.current + return { keyboardController?.hide() } +} + +/** + * Creates a [LazyListState] that is remembered across compositions. + * + * And when user scrolling the lazy list, hides the keyboard if it's open. + */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun rememberLazyListStateAndHideKeyboardWhenStartScroll(): LazyListState { + val listState = rememberLazyListState() + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(listState) { + snapshotFlow { listState.isScrollInProgress } + .distinctUntilChanged() + .filter { it } + .collect { keyboardController?.hide() } + } + return listState +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt index 69ddf01b6170d..c8faef6d67034 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt @@ -21,4 +21,5 @@ object SettingsOpacity { const val Disabled = 0.38f const val Divider = 0.2f const val SurfaceTone = 0.14f + const val Hint = 0.9f } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt index 6a88f2dd369ce..764973f2a8f51 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt @@ -16,9 +16,12 @@ package com.android.settingslib.spa.widget.scaffold +import androidx.appcompat.R import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material.icons.outlined.FindInPage import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.DropdownMenu import androidx.compose.material3.Icon @@ -31,17 +34,23 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import com.android.settingslib.spa.framework.compose.LocalNavController +/** Action that navigates back to last page. */ @Composable internal fun NavigateBack() { val navController = LocalNavController.current - val contentDescription = stringResource( - id = androidx.appcompat.R.string.abc_action_bar_up_description, - ) + val contentDescription = stringResource(R.string.abc_action_bar_up_description) BackAction(contentDescription) { navController.navigateBack() } } +/** Action that collapses the search bar. */ +@Composable +internal fun CollapseAction(onClick: () -> Unit) { + val contentDescription = stringResource(R.string.abc_toolbar_collapse_description) + BackAction(contentDescription, onClick) +} + @Composable private fun BackAction(contentDescription: String, onClick: () -> Unit) { IconButton(onClick) { @@ -52,6 +61,28 @@ private fun BackAction(contentDescription: String, onClick: () -> Unit) { } } +/** Action that expends the search bar. */ +@Composable +internal fun SearchAction(onClick: () -> Unit) { + IconButton(onClick) { + Icon( + imageVector = Icons.Outlined.FindInPage, + contentDescription = stringResource(R.string.search_menu_title), + ) + } +} + +/** Action that clear the search query. */ +@Composable +internal fun ClearAction(onClick: () -> Unit) { + IconButton(onClick) { + Icon( + imageVector = Icons.Outlined.Clear, + contentDescription = stringResource(R.string.abc_searchview_description_clear), + ) + } +} + @Composable fun MoreOptionsAction( content: @Composable ColumnScope.(onDismissRequest: () -> Unit) -> Unit, @@ -71,9 +102,7 @@ private fun MoreOptionsActionButton(onClick: () -> Unit) { IconButton(onClick) { Icon( imageVector = Icons.Outlined.MoreVert, - contentDescription = stringResource( - id = androidx.appcompat.R.string.abc_action_menu_overflow_description, - ) + contentDescription = stringResource(R.string.abc_action_menu_overflow_description), ) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt new file mode 100644 index 0000000000000..4f83ad6bd2912 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt @@ -0,0 +1,189 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.android.settingslib.spa.widget.scaffold + +import androidx.activity.compose.BackHandler +import androidx.appcompat.R +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import com.android.settingslib.spa.framework.compose.hideKeyboardAction +import com.android.settingslib.spa.framework.theme.SettingsOpacity +import com.android.settingslib.spa.framework.theme.SettingsTheme + +/** + * A [Scaffold] which content is can be full screen, and with a search feature built-in. + */ +@Composable +fun SearchScaffold( + title: String, + actions: @Composable RowScope.() -> Unit = {}, + content: @Composable (searchQuery: State) -> Unit, +) { + val viewModel: SearchScaffoldViewModel = viewModel() + + Scaffold( + topBar = { + SearchableTopAppBar( + title = title, + actions = actions, + searchQuery = viewModel.searchQuery, + ) { viewModel.searchQuery = it } + }, + ) { paddingValues -> + Box( + Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + val searchQuery = remember { + derivedStateOf { viewModel.searchQuery?.text ?: "" } + } + content(searchQuery) + } + } +} + +internal class SearchScaffoldViewModel : ViewModel() { + var searchQuery: TextFieldValue? by mutableStateOf(null) +} + +@Composable +private fun SearchableTopAppBar( + title: String, + actions: @Composable RowScope.() -> Unit, + searchQuery: TextFieldValue?, + onSearchQueryChange: (TextFieldValue?) -> Unit, +) { + if (searchQuery != null) { + SearchTopAppBar( + query = searchQuery, + onQueryChange = onSearchQueryChange, + onClose = { onSearchQueryChange(null) }, + actions = actions, + ) + } else { + SettingsTopAppBar(title) { + SearchAction { onSearchQueryChange(TextFieldValue()) } + actions() + } + } +} + +@Composable +private fun SearchTopAppBar( + query: TextFieldValue, + onQueryChange: (TextFieldValue) -> Unit, + onClose: () -> Unit, + actions: @Composable RowScope.() -> Unit = {}, +) { + TopAppBar( + title = { SearchBox(query, onQueryChange) }, + modifier = Modifier.statusBarsPadding(), + navigationIcon = { CollapseAction(onClose) }, + actions = { + if (query.text.isNotEmpty()) { + ClearAction { onQueryChange(TextFieldValue()) } + } + actions() + }, + colors = settingsTopAppBarColors(), + ) + BackHandler { onClose() } +} + +@Composable +private fun SearchBox(query: TextFieldValue, onQueryChange: (TextFieldValue) -> Unit) { + val focusRequester = remember { FocusRequester() } + val textStyle = MaterialTheme.typography.bodyLarge + TextField( + value = query, + onValueChange = onQueryChange, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + textStyle = textStyle, + placeholder = { + Text( + text = stringResource(R.string.abc_search_hint), + modifier = Modifier.alpha(SettingsOpacity.Hint), + style = textStyle, + ) + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = hideKeyboardAction()), + singleLine = true, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) + + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + } +} + +@Preview +@Composable +private fun SearchTopAppBarPreview() { + SettingsTheme { + SearchTopAppBar(query = TextFieldValue(), onQueryChange = {}, onClose = {}) {} + } +} + +@Preview +@Composable +private fun SearchScaffoldPreview() { + SettingsTheme { + SearchScaffold(title = "App notifications") {} + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt index b50cb3ed84d0a..3bc3dd72d3532 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt @@ -18,17 +18,10 @@ package com.android.settingslib.spa.widget.scaffold import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme /** @@ -42,32 +35,11 @@ fun SettingsScaffold( content: @Composable (PaddingValues) -> Unit, ) { Scaffold( - topBar = { - TopAppBar( - title = { - Text( - text = title, - modifier = Modifier.padding(SettingsDimension.itemPaddingAround), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - }, - navigationIcon = { NavigateBack() }, - actions = actions, - colors = settingsTopAppBarColors(), - ) - }, + topBar = { SettingsTopAppBar(title, actions) }, content = content, ) } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun settingsTopAppBarColors() = TopAppBarDefaults.smallTopAppBarColors( - containerColor = SettingsTheme.colorScheme.surfaceHeader, - scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader, -) - @Preview @Composable private fun SettingsScaffoldPreview() { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt new file mode 100644 index 0000000000000..93535203b1b94 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt @@ -0,0 +1,57 @@ +/* + * 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.settingslib.spa.widget.scaffold + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SettingsTopAppBar( + title: String, + actions: @Composable RowScope.() -> Unit, +) { + TopAppBar( + title = { + Text( + text = title, + modifier = Modifier.padding(SettingsDimension.itemPaddingAround), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + }, + navigationIcon = { NavigateBack() }, + actions = actions, + colors = settingsTopAppBarColors(), + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun settingsTopAppBarColors() = TopAppBarDefaults.smallTopAppBarColors( + containerColor = SettingsTheme.colorScheme.surfaceHeader, + scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader, +) diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/com/android/settingslib/spa/framework/OverridableFlowTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt similarity index 92% rename from packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/com/android/settingslib/spa/framework/OverridableFlowTest.kt rename to packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt index 8f460f30cf364..c94572b16198f 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/com/android/settingslib/spa/framework/OverridableFlowTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt @@ -14,10 +14,9 @@ * limitations under the License. */ -package com.android.settingslib.spa.framework.compose.com.android.settingslib.spa.framework +package com.android.settingslib.spa.framework.compose import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.framework.compose.OverridableFlow import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt new file mode 100644 index 0000000000000..ec3379dd46eeb --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt @@ -0,0 +1,146 @@ +/* + * 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.settingslib.spa.widget.scaffold + +import android.content.Context +import androidx.appcompat.R +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.State +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SearchScaffoldTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Test + fun initialState_titleIsDisplayed() { + composeTestRule.setContent { + SearchScaffold(title = TITLE) {} + } + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() + } + + @Test + fun initialState_clearButtonNotExist() { + setContent() + + onClearButton().assertDoesNotExist() + } + + @Test + fun initialState_searchQueryIsEmpty() { + val searchQuery = setContent() + + assertThat(searchQuery.value).isEqualTo("") + } + + @Test + fun canEnterSearchMode() { + val searchQuery = setContent() + + clickSearchButton() + + composeTestRule.onNodeWithText(TITLE).assertDoesNotExist() + onSearchHint().assertIsDisplayed() + onClearButton().assertDoesNotExist() + assertThat(searchQuery.value).isEqualTo("") + } + + @Test + fun canExitSearchMode() { + val searchQuery = setContent() + + clickSearchButton() + composeTestRule.onNodeWithContentDescription( + context.getString(R.string.abc_toolbar_collapse_description) + ).performClick() + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() + onSearchHint().assertDoesNotExist() + onClearButton().assertDoesNotExist() + assertThat(searchQuery.value).isEqualTo("") + } + + @Test + fun canEnterSearchQuery() { + val searchQuery = setContent() + + clickSearchButton() + onSearchHint().performTextInput(QUERY) + + onClearButton().assertIsDisplayed() + assertThat(searchQuery.value).isEqualTo(QUERY) + } + + @Test + fun canClearSearchQuery() { + val searchQuery = setContent() + + clickSearchButton() + onSearchHint().performTextInput(QUERY) + onClearButton().performClick() + + onClearButton().assertDoesNotExist() + assertThat(searchQuery.value).isEqualTo("") + } + + private fun setContent(): State { + lateinit var actualSearchQuery: State + composeTestRule.setContent { + SearchScaffold(title = TITLE) { searchQuery -> + SideEffect { + actualSearchQuery = searchQuery + } + } + } + return actualSearchQuery + } + + private fun clickSearchButton() { + composeTestRule.onNodeWithContentDescription( + context.getString(R.string.search_menu_title) + ).performClick() + } + + private fun onSearchHint() = composeTestRule.onNodeWithText( + context.getString(R.string.abc_search_hint) + ) + + private fun onClearButton() = composeTestRule.onNodeWithContentDescription( + context.getString(R.string.abc_searchview_description_clear) + ) + + private companion object { + const val TITLE = "title" + const val QUERY = "query" + } +} 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 c5ad1814a5180..408b9df5e3ef6 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 @@ -19,7 +19,6 @@ package com.android.settingslib.spaprivileged.template.app import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState @@ -29,6 +28,7 @@ import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel 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.compose.toState import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.widget.ui.PlaceholderTitle @@ -76,7 +76,7 @@ private fun AppListWidget( } LazyColumn( modifier = Modifier.fillMaxSize(), - state = rememberLazyListState(), + state = rememberLazyListStateAndHideKeyboardWhenStartScroll(), contentPadding = PaddingValues(bottom = SettingsDimension.itemPaddingVertical), ) { items(count = list.size, key = { option to list[it].record.app.packageName }) { diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt index 0caa8213e1630..99376b0005e42 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt @@ -16,10 +16,8 @@ package com.android.settingslib.spaprivileged.template.app -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -28,9 +26,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction -import com.android.settingslib.spa.widget.scaffold.SettingsScaffold +import com.android.settingslib.spa.widget.scaffold.SearchScaffold import com.android.settingslib.spa.widget.ui.Spinner import com.android.settingslib.spaprivileged.R import com.android.settingslib.spaprivileged.model.app.AppListConfig @@ -50,35 +47,28 @@ fun AppListPage( appItem: @Composable (itemState: AppListItemModel) -> Unit, ) { val showSystem = rememberSaveable { mutableStateOf(false) } - // TODO: Use SearchScaffold here. - SettingsScaffold( + SearchScaffold( title = title, actions = { ShowSystemAction(showSystem.value) { showSystem.value = it } }, - ) { paddingValues -> - Box( - Modifier - .padding(paddingValues) - .fillMaxSize() - ) { - WorkProfilePager(primaryUserOnly) { userInfo -> - Column(Modifier.fillMaxSize()) { - val options = remember { listModel.getSpinnerOptions() } - val selectedOption = rememberSaveable { mutableStateOf(0) } - Spinner(options, selectedOption.value) { selectedOption.value = it } - AppList( - appListConfig = AppListConfig( - userId = userInfo.id, - showInstantApps = showInstantApps, - ), - listModel = listModel, - showSystem = showSystem, - option = selectedOption, - searchQuery = stateOf(""), - appItem = appItem, - ) - } + ) { searchQuery -> + WorkProfilePager(primaryUserOnly) { userInfo -> + Column(Modifier.fillMaxSize()) { + val options = remember { listModel.getSpinnerOptions() } + val selectedOption = rememberSaveable { mutableStateOf(0) } + Spinner(options, selectedOption.value) { selectedOption.value = it } + AppList( + appListConfig = AppListConfig( + userId = userInfo.id, + showInstantApps = showInstantApps, + ), + listModel = listModel, + showSystem = showSystem, + option = selectedOption, + searchQuery = searchQuery, + appItem = appItem, + ) } } } From 7bae0f83e64c3fae8701c48b473280963f3209a6 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 25 Oct 2022 17:26:42 +0800 Subject: [PATCH 3/3] Add SettingsFontFamily for SpaLib So we could get the font family from config and apply them to SPA. Bug: 235727273 Test: Manual with Settings App Change-Id: I2af44e3a73db2c1a1d21630b6c16cb58a745c4b1 --- .../spa/framework/theme/SettingsFontFamily.kt | 65 +++++++++++++++++++ .../spa/framework/theme/SettingsTypography.kt | 10 +-- 2 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt new file mode 100644 index 0000000000000..8e8805aa49d24 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt @@ -0,0 +1,65 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalTextApi::class) + +package com.android.settingslib.spa.framework.theme + +import android.annotation.SuppressLint +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.font.DeviceFontFamilyName +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight + +internal data class SettingsFontFamily( + val brand: FontFamily = FontFamily.Default, + val plain: FontFamily = FontFamily.Default, +) + +private fun Context.getSettingsFontFamily(inInspection: Boolean): SettingsFontFamily { + if (inInspection) { + return SettingsFontFamily() + } + return SettingsFontFamily( + brand = FontFamily( + Font(getFontFamilyName("config_headlineFontFamily"), FontWeight.Normal), + Font(getFontFamilyName("config_headlineFontFamilyMedium"), FontWeight.Medium), + ), + plain = FontFamily( + Font(getFontFamilyName("config_bodyFontFamily"), FontWeight.Normal), + Font(getFontFamilyName("config_bodyFontFamilyMedium"), FontWeight.Medium), + ), + ) +} + +private fun Context.getFontFamilyName(configName: String): DeviceFontFamilyName { + @SuppressLint("DiscouragedApi") + val configId = resources.getIdentifier(configName, "string", "android") + return DeviceFontFamilyName(resources.getString(configId)) +} + +@Composable +internal fun rememberSettingsFontFamily(): SettingsFontFamily { + val context = LocalContext.current + val inInspection = LocalInspectionMode.current + return remember { context.getSettingsFontFamily(inInspection) } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt index 07f09ba95ca31..03699bf4d4c00 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt @@ -20,14 +20,13 @@ import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp -private class SettingsTypography { - private val brand = FontFamily.Default - private val plain = FontFamily.Default +private class SettingsTypography(settingsFontFamily: SettingsFontFamily) { + private val brand = settingsFontFamily.brand + private val plain = settingsFontFamily.plain val typography = Typography( displayLarge = TextStyle( @@ -140,5 +139,6 @@ private class SettingsTypography { @Composable internal fun rememberSettingsTypography(): Typography { - return remember { SettingsTypography().typography } + val settingsFontFamily = rememberSettingsFontFamily() + return remember { SettingsTypography(settingsFontFamily).typography } }