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:
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") {}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user