diff options
17 files changed, 578 insertions, 67 deletions
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle index 68c63dad6726..4fb77d7257ca 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 e09ebdad7a1f..b38178b0e6f4 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 3bafcf27fed9..2820ed7bc96e 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/Keyboards.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt new file mode 100644 index 000000000000..8d0313fdce9f --- /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/compose/Pager.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt index bf338574c42d..4df7794e8759 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/framework/theme/SettingsFontFamily.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt new file mode 100644 index 000000000000..8e8805aa49d2 --- /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/SettingsOpacity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt index 69ddf01b6170..c8faef6d6703 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/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt index 07f09ba95ca3..03699bf4d4c0 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 } } 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 6a88f2dd369c..764973f2a8f5 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 000000000000..4f83ad6bd291 --- /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<String>) -> 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 d17e4645ee5e..3bc3dd72d353 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.SmallTopAppBar -import androidx.compose.material3.Text -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 = { - SmallTopAppBar( - 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 -internal fun settingsTopAppBarColors() = TopAppBarDefaults.largeTopAppBarColors( - 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 000000000000..93535203b1b9 --- /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/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 b9690762845e..9831b918894b 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<Boolean?>, changeable: State<Boolean>, 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/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 index 8f460f30cf36..c94572b16198 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 000000000000..ec3379dd46ee --- /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<String> { + lateinit var actualSearchQuery: State<String> + 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 c5ad1814a518..408b9df5e3ef 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 <T : AppRecord> 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 2be1d1c6cce6..99376b0005e4 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 @@ -17,9 +17,7 @@ package com.android.settingslib.spaprivileged.template.app 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 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,14 +47,12 @@ fun <T : AppRecord> AppListPage( appItem: @Composable (itemState: AppListItemModel<T>) -> Unit, ) { val showSystem = rememberSaveable { mutableStateOf(false) } - // TODO: Use SearchScaffold here. - SettingsScaffold( + SearchScaffold( title = title, actions = { ShowSystemAction(showSystem.value) { showSystem.value = it } }, - ) { paddingValues -> - Spacer(Modifier.padding(paddingValues)) + ) { searchQuery -> WorkProfilePager(primaryUserOnly) { userInfo -> Column(Modifier.fillMaxSize()) { val options = remember { listModel.getSpinnerOptions() } @@ -71,7 +66,7 @@ fun <T : AppRecord> AppListPage( listModel = listModel, showSystem = showSystem, option = selectedOption, - searchQuery = stateOf(""), + searchQuery = searchQuery, appItem = appItem, ) } |