Merge changes I2af44e3a,If75226f3,I0a5cbf01

* changes:
  Add SettingsFontFamily for SpaLib
  Add SearchScaffold for SpaLib
  Upgrade Compose version to 1.3 for SpaLib
This commit is contained in:
Chaohui Wang
2022-10-25 12:24:44 +00:00
committed by Android (Google) Code Review
17 changed files with 578 additions and 67 deletions

View File

@@ -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 {

View File

@@ -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")
}
}
}
}

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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(

View File

@@ -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) }
}

View File

@@ -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
}

View File

@@ -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 }
}

View File

@@ -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),
)
}
}

View File

@@ -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") {}
}
}

View File

@@ -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() {

View File

@@ -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,
)

View File

@@ -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,

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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 }) {

View File

@@ -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,
)
}