diff options
17 files changed, 391 insertions, 116 deletions
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index 2820ed7bc96e..3b159e95cc55 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -59,9 +59,9 @@ dependencies { 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" - api "androidx.lifecycle:lifecycle-livedata-ktx:2.6.0-alpha02" + api "androidx.lifecycle:lifecycle-livedata-ktx:2.6.0-alpha03" api "androidx.navigation:navigation-compose:2.5.0" - api "com.google.android.material:material:1.6.1" + api "com.google.android.material:material:1.7.0-alpha03" debugApi "androidx.compose.ui:ui-tooling:$jetpack_compose_version" implementation "com.airbnb.android:lottie-compose:5.2.0" } diff --git a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml b/packages/SettingsLib/Spa/spa/res/values-night/themes.xml deleted file mode 100644 index 67dd2b0cc5e0..000000000000 --- a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - 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. ---> -<resources> - - <style name="Theme.SpaLib.DayNight" /> -</resources> diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml index e0e5fc211ec6..25846ec2d20b 100644 --- a/packages/SettingsLib/Spa/spa/res/values/themes.xml +++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml @@ -16,12 +16,10 @@ --> <resources> - <style name="Theme.SpaLib" parent="Theme.Material3.DayNight.NoActionBar"> + <style name="Theme.SpaLib" parent="@android:style/Theme.DeviceDefault.Settings"> <item name="android:statusBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item> - </style> - - <style name="Theme.SpaLib.DayNight"> - <item name="android:windowLightStatusBar">true</item> + <item name="android:windowActionBar">false</item> + <item name="android:windowNoTitle">true</item> </style> </resources> diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt index d3efaa7480f8..c3c90ab4fdb8 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.core.view.WindowCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.navigation.NavGraph.Companion.findStartDestination @@ -66,8 +67,9 @@ open class BrowseActivity : ComponentActivity() { private val spaEnvironment get() = SpaEnvironmentFactory.instance override fun onCreate(savedInstanceState: Bundle?) { - setTheme(R.style.Theme_SpaLib_DayNight) + setTheme(R.style.Theme_SpaLib) super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK) setContent { @@ -83,35 +85,19 @@ open class BrowseActivity : ComponentActivity() { val navController = rememberNavController() val nullPage = SettingsPage.createNull() CompositionLocalProvider(navController.localNavController()) { - NavHost(navController, nullPage.sppName) { + NavHost( + navController = navController, + startDestination = nullPage.sppName, + ) { composable(nullPage.sppName) {} for (spp in sppRepository.getAllProviders()) { composable( route = spp.name + spp.parameter.navRoute(), arguments = spp.parameter, ) { navBackStackEntry -> - val lifecycleOwner = LocalLifecycleOwner.current - val sp = remember(navBackStackEntry.arguments) { + PageLogger(remember(navBackStackEntry.arguments) { spp.createSettingsPage(arguments = navBackStackEntry.arguments) - } - - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_START) { - sp.enterPage() - } else if (event == Lifecycle.Event.ON_STOP) { - sp.leavePage() - } - } - - // Add the observer to the lifecycle - lifecycleOwner.lifecycle.addObserver(observer) - - // When the effect leaves the Composition, remove the observer - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - } + }) spp.Page(navBackStackEntry.arguments) } @@ -122,6 +108,28 @@ open class BrowseActivity : ComponentActivity() { } @Composable + private fun PageLogger(settingsPage: SettingsPage) { + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START) { + settingsPage.enterPage() + } else if (event == Lifecycle.Event.ON_STOP) { + settingsPage.leavePage() + } + } + + // Add the observer to the lifecycle + lifecycleOwner.lifecycle.addObserver(observer) + + // When the effect leaves the Composition, remove the observer + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + } + + @Composable private fun InitialDestinationNavigator() { val sppRepository by spaEnvironment.pageProviderRepository val destinationNavigated = rememberSaveable { mutableStateOf(false) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/PaddingValuesExt.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/PaddingValuesExt.kt new file mode 100644 index 000000000000..18335ff6eba5 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/PaddingValuesExt.kt @@ -0,0 +1,48 @@ +/* + * 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.layout.PaddingValues +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp + +internal fun PaddingValues.horizontalValues(): PaddingValues = HorizontalPaddingValues(this) + +internal fun PaddingValues.verticalValues(): PaddingValues = VerticalPaddingValues(this) + +private class HorizontalPaddingValues(private val paddingValues: PaddingValues) : PaddingValues { + override fun calculateLeftPadding(layoutDirection: LayoutDirection) = + paddingValues.calculateLeftPadding(layoutDirection) + + override fun calculateTopPadding(): Dp = 0.dp + + override fun calculateRightPadding(layoutDirection: LayoutDirection) = + paddingValues.calculateRightPadding(layoutDirection) + + override fun calculateBottomPadding() = 0.dp +} + +private class VerticalPaddingValues(private val paddingValues: PaddingValues) : PaddingValues { + override fun calculateLeftPadding(layoutDirection: LayoutDirection) = 0.dp + + override fun calculateTopPadding(): Dp = paddingValues.calculateTopPadding() + + override fun calculateRightPadding(layoutDirection: LayoutDirection) = 0.dp + + override fun calculateBottomPadding() = paddingValues.calculateBottomPadding() +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt index 9eaa88ae3168..26491d51e838 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt @@ -62,7 +62,7 @@ class DebugActivity : ComponentActivity() { private val spaEnvironment get() = SpaEnvironmentFactory.instance override fun onCreate(savedInstanceState: Bundle?) { - setTheme(R.style.Theme_SpaLib_DayNight) + setTheme(R.style.Theme_SpaLib) super.onCreate(savedInstanceState) spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt index eb20ac5c5f09..711c8a753532 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme @@ -34,6 +35,7 @@ fun HomeScaffold(title: String, content: @Composable () -> Unit) { Modifier .fillMaxSize() .background(color = MaterialTheme.colorScheme.background) + .systemBarsPadding() .verticalScroll(rememberScrollState()), ) { Text( diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt index 9a17b2a8cb78..d17a8dcd0161 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt @@ -19,7 +19,7 @@ package com.android.settingslib.spa.widget.scaffold import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold @@ -27,6 +27,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel /** * A [Scaffold] which content is scrollable and wrapped in a [Column]. @@ -42,8 +44,9 @@ fun RegularScaffold( ) { SettingsScaffold(title, actions) { paddingValues -> Column(Modifier.verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(paddingValues)) + Spacer(Modifier.height(paddingValues.calculateTopPadding())) content() + Spacer(Modifier.height(paddingValues.calculateBottomPadding())) } } } @@ -52,6 +55,13 @@ fun RegularScaffold( @Composable private fun RegularScaffoldPreview() { SettingsTheme { - RegularScaffold(title = "Display") {} + RegularScaffold(title = "Display") { + Preference(object : PreferenceModel { + override val title = "Item 1" + }) + Preference(object : PreferenceModel { + override val title = "Item 2" + }) + } } } 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 index 4f83ad6bd291..efc623af9cc0 100644 --- 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 @@ -14,13 +14,12 @@ * 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.Column import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -31,10 +30,13 @@ 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.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State @@ -48,45 +50,57 @@ 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.input.nestedscroll.nestedScroll 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.compose.ui.unit.Dp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import com.android.settingslib.spa.framework.compose.hideKeyboardAction +import com.android.settingslib.spa.framework.compose.horizontalValues import com.android.settingslib.spa.framework.theme.SettingsOpacity import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel /** * A [Scaffold] which content is can be full screen, and with a search feature built-in. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchScaffold( title: String, actions: @Composable RowScope.() -> Unit = {}, - content: @Composable (searchQuery: State<String>) -> Unit, + content: @Composable (bottomPadding: Dp, searchQuery: State<String>) -> Unit, ) { val viewModel: SearchScaffoldViewModel = viewModel() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { SearchableTopAppBar( title = title, actions = actions, + scrollBehavior = scrollBehavior, searchQuery = viewModel.searchQuery, ) { viewModel.searchQuery = it } }, ) { paddingValues -> Box( Modifier - .padding(paddingValues) - .fillMaxSize() + .padding(paddingValues.horizontalValues()) + .padding(top = paddingValues.calculateTopPadding()) + .fillMaxSize(), ) { - val searchQuery = remember { - derivedStateOf { viewModel.searchQuery?.text ?: "" } - } - content(searchQuery) + content( + bottomPadding = paddingValues.calculateBottomPadding(), + searchQuery = remember { + derivedStateOf { viewModel.searchQuery?.text ?: "" } + }, + ) } } } @@ -95,10 +109,12 @@ internal class SearchScaffoldViewModel : ViewModel() { var searchQuery: TextFieldValue? by mutableStateOf(null) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun SearchableTopAppBar( title: String, actions: @Composable RowScope.() -> Unit, + scrollBehavior: TopAppBarScrollBehavior, searchQuery: TextFieldValue?, onSearchQueryChange: (TextFieldValue?) -> Unit, ) { @@ -110,13 +126,17 @@ private fun SearchableTopAppBar( actions = actions, ) } else { - SettingsTopAppBar(title) { - SearchAction { onSearchQueryChange(TextFieldValue()) } + SettingsTopAppBar(title, scrollBehavior) { + SearchAction { + scrollBehavior.collapse() + onSearchQueryChange(TextFieldValue()) + } actions() } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun SearchTopAppBar( query: TextFieldValue, @@ -124,21 +144,24 @@ private fun SearchTopAppBar( 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(), - ) + Surface(color = SettingsTheme.colorScheme.surfaceHeader) { + TopAppBar( + title = { SearchBox(query, onQueryChange) }, + modifier = Modifier.statusBarsPadding(), + navigationIcon = { CollapseAction(onClose) }, + actions = { + if (query.text.isNotEmpty()) { + ClearAction { onQueryChange(TextFieldValue()) } + } + actions() + }, + colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = Color.Transparent), + ) + } BackHandler { onClose() } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun SearchBox(query: TextFieldValue, onQueryChange: (TextFieldValue) -> Unit) { val focusRequester = remember { FocusRequester() } @@ -184,6 +207,15 @@ private fun SearchTopAppBarPreview() { @Composable private fun SearchScaffoldPreview() { SettingsTheme { - SearchScaffold(title = "App notifications") {} + SearchScaffold(title = "App notifications") { _, _ -> + Column { + Preference(object : PreferenceModel { + override val title = "Item 1" + }) + Preference(object : PreferenceModel { + override val title = "Item 2" + }) + } + } } } 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 3bc3dd72d353..f4e504a954a2 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 @@ -16,13 +16,23 @@ package com.android.settingslib.spa.widget.scaffold +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column 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.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.compose.horizontalValues +import com.android.settingslib.spa.framework.compose.verticalValues import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel /** * A [Scaffold] which content is can be full screen when needed. @@ -34,16 +44,30 @@ fun SettingsScaffold( actions: @Composable RowScope.() -> Unit = {}, content: @Composable (PaddingValues) -> Unit, ) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( - topBar = { SettingsTopAppBar(title, actions) }, - content = content, - ) + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { SettingsTopAppBar(title, scrollBehavior, actions) }, + ) { paddingValues -> + Box(Modifier.padding(paddingValues.horizontalValues())) { + content(paddingValues.verticalValues()) + } + } } @Preview @Composable private fun SettingsScaffoldPreview() { SettingsTheme { - SettingsScaffold(title = "Display") {} + SettingsScaffold(title = "Display") { paddingValues -> + Column(Modifier.padding(paddingValues)) { + Preference(object : PreferenceModel { + override val title = "Item 1" + }) + Preference(object : PreferenceModel { + override val title = "Item 2" + }) + } + } } } 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 index 93535203b1b9..f7cb035cbf93 100644 --- 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 @@ -17,41 +17,70 @@ package com.android.settingslib.spa.widget.scaffold import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow +import com.android.settingslib.spa.framework.compose.horizontalValues import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.theme.rememberSettingsTypography @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun SettingsTopAppBar( title: String, + scrollBehavior: TopAppBarScrollBehavior, 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(), + val colorScheme = MaterialTheme.colorScheme + // TODO: Remove MaterialTheme() after top app bar color fixed in AndroidX. + MaterialTheme( + colorScheme = remember { colorScheme.copy(surface = colorScheme.background) }, + typography = rememberSettingsTypography(), + ) { + LargeTopAppBar( + title = { Title(title) }, + navigationIcon = { NavigateBack() }, + actions = actions, + colors = largeTopAppBarColors(), + scrollBehavior = scrollBehavior, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +internal fun TopAppBarScrollBehavior.collapse() { + with(state) { + heightOffset = heightOffsetLimit + } +} + +@Composable +private fun Title(title: String) { + Text( + text = title, + modifier = Modifier + .padding(WindowInsets.navigationBars.asPaddingValues().horizontalValues()) + .padding(SettingsDimension.itemPaddingAround), + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun settingsTopAppBarColors() = TopAppBarDefaults.smallTopAppBarColors( - containerColor = SettingsTheme.colorScheme.surfaceHeader, +private fun largeTopAppBarColors() = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.background, scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader, ) diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/RegularScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/RegularScaffoldTest.kt new file mode 100644 index 000000000000..1964c436d138 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/RegularScaffoldTest.kt @@ -0,0 +1,61 @@ +/* + * 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.material3.Text +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RegularScaffoldTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun regularScaffold_titleIsDisplayed() { + composeTestRule.setContent { + RegularScaffold(title = TITLE) { + Text(text = "AAA") + Text(text = "BBB") + } + } + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() + } + + @Test + fun regularScaffold_itemsAreDisplayed() { + composeTestRule.setContent { + RegularScaffold(title = TITLE) { + Text(text = "AAA") + Text(text = "BBB") + } + } + + composeTestRule.onNodeWithText("AAA").assertIsDisplayed() + composeTestRule.onNodeWithText("BBB").assertIsDisplayed() + } + + private companion object { + const val TITLE = "title" + } +} 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 index ec3379dd46ee..c3e1d544a6ab 100644 --- 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 @@ -43,7 +43,7 @@ class SearchScaffoldTest { @Test fun initialState_titleIsDisplayed() { composeTestRule.setContent { - SearchScaffold(title = TITLE) {} + SearchScaffold(title = TITLE) { _, _ -> } } composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() @@ -116,7 +116,7 @@ class SearchScaffoldTest { private fun setContent(): State<String> { lateinit var actualSearchQuery: State<String> composeTestRule.setContent { - SearchScaffold(title = TITLE) { searchQuery -> + SearchScaffold(title = TITLE) { _, searchQuery -> SideEffect { actualSearchQuery = searchQuery } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerTest.kt index 0c84eac45cb7..0c745d5d5b3d 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerTest.kt @@ -16,7 +16,6 @@ package com.android.settingslib.spa.widget.scaffold -import androidx.compose.runtime.Composable import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertIsNotSelected @@ -31,15 +30,13 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class SettingsPagerKtTest { +class SettingsPagerTest { @get:Rule val composeTestRule = createComposeRule() @Test fun twoPage_initialState() { - composeTestRule.setContent { - TestTwoPage() - } + setTwoPagesContent() composeTestRule.onNodeWithText("Personal").assertIsSelected() composeTestRule.onNodeWithText("Page 0").assertIsDisplayed() @@ -49,9 +46,7 @@ class SettingsPagerKtTest { @Test fun twoPage_afterSwitch() { - composeTestRule.setContent { - TestTwoPage() - } + setTwoPagesContent() composeTestRule.onNodeWithText("Work").performClick() @@ -73,11 +68,12 @@ class SettingsPagerKtTest { composeTestRule.onNodeWithText("Page 0").assertIsDisplayed() composeTestRule.onNodeWithText("Page 1").assertDoesNotExist() } -} -@Composable -private fun TestTwoPage() { - SettingsPager(listOf("Personal", "Work")) { - SettingsTitle(title = "Page $it") + private fun setTwoPagesContent() { + composeTestRule.setContent { + SettingsPager(listOf("Personal", "Work")) { + SettingsTitle(title = "Page $it") + } + } } } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt new file mode 100644 index 000000000000..f04240485386 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt @@ -0,0 +1,84 @@ +/* + * 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.PaddingValues +import androidx.compose.material3.Text +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +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 SettingsScaffoldTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun settingsScaffold_titleIsDisplayed() { + composeTestRule.setContent { + SettingsScaffold(title = TITLE) { + Text(text = "AAA") + Text(text = "BBB") + } + } + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() + } + + @Test + fun settingsScaffold_itemsAreDisplayed() { + composeTestRule.setContent { + SettingsScaffold(title = TITLE) { + Text(text = "AAA") + Text(text = "BBB") + } + } + + composeTestRule.onNodeWithText("AAA").assertIsDisplayed() + composeTestRule.onNodeWithText("BBB").assertIsDisplayed() + } + + @Test + fun settingsScaffold_noHorizontalPadding() { + lateinit var actualPaddingValues: PaddingValues + + composeTestRule.setContent { + SettingsScaffold(title = TITLE) { paddingValues -> + SideEffect { + actualPaddingValues = paddingValues + } + } + } + + assertThat(actualPaddingValues.calculateLeftPadding(LayoutDirection.Ltr)).isEqualTo(0.dp) + assertThat(actualPaddingValues.calculateLeftPadding(LayoutDirection.Rtl)).isEqualTo(0.dp) + assertThat(actualPaddingValues.calculateRightPadding(LayoutDirection.Ltr)).isEqualTo(0.dp) + assertThat(actualPaddingValues.calculateRightPadding(LayoutDirection.Rtl)).isEqualTo(0.dp) + } + + private companion object { + const val TITLE = "title" + } +} 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 408b9df5e3ef..3cd8378b8960 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 @@ -25,12 +25,12 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp 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 import com.android.settingslib.spaprivileged.R import com.android.settingslib.spaprivileged.model.app.AppListConfig @@ -55,10 +55,11 @@ internal fun <T : AppRecord> AppList( option: State<Int>, searchQuery: State<String>, appItem: @Composable (itemState: AppListItemModel<T>) -> Unit, + bottomPadding: Dp, ) { LogCompositions(TAG, appListConfig.userId.toString()) val appListData = loadAppEntries(appListConfig, listModel, showSystem, option, searchQuery) - AppListWidget(appListData, listModel, appItem) + AppListWidget(appListData, listModel, appItem, bottomPadding) } @Composable @@ -66,6 +67,7 @@ private fun <T : AppRecord> AppListWidget( appListData: State<AppListData<T>?>, listModel: AppListModel<T>, appItem: @Composable (itemState: AppListItemModel<T>) -> Unit, + bottomPadding: Dp, ) { val timeMeasurer = rememberTimeMeasurer(TAG) appListData.value?.let { (list, option) -> @@ -77,7 +79,7 @@ private fun <T : AppRecord> AppListWidget( LazyColumn( modifier = Modifier.fillMaxSize(), state = rememberLazyListStateAndHideKeyboardWhenStartScroll(), - contentPadding = PaddingValues(bottom = SettingsDimension.itemPaddingVertical), + contentPadding = PaddingValues(bottom = bottomPadding), ) { items(count = list.size, key = { option to list[it].record.app.packageName }) { val appEntry = list[it] 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 99376b0005e4..29533679d9c1 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 @@ -52,7 +52,7 @@ fun <T : AppRecord> AppListPage( actions = { ShowSystemAction(showSystem.value) { showSystem.value = it } }, - ) { searchQuery -> + ) { bottomPadding, searchQuery -> WorkProfilePager(primaryUserOnly) { userInfo -> Column(Modifier.fillMaxSize()) { val options = remember { listModel.getSpinnerOptions() } @@ -68,6 +68,7 @@ fun <T : AppRecord> AppListPage( option = selectedOption, searchQuery = searchQuery, appItem = appItem, + bottomPadding = bottomPadding, ) } } |