Merge "Use LargeTopAppBar for SpaLib"
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user