Use LargeTopAppBar for SpaLib

To support collapsable top app bar.

Bug: 235727273
Test: Unit test
Test: Manual with Gallery
Change-Id: I1eeee7be023556c54681ef06c070d2f2c864940b
This commit is contained in:
Chaohui Wang
2022-10-26 23:19:47 +08:00
parent b9de83efae
commit fa7531bc45
17 changed files with 391 additions and 116 deletions

View File

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

View File

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

View File

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

View File

@@ -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)
}
@@ -121,6 +107,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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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