Merge changes Id5a3990b,I3121f9a3

* changes:
  Add Footer for SpaLib
  Add SwitchPreference for SpaLib
This commit is contained in:
Chaohui Wang
2022-08-15 15:06:03 +00:00
committed by Android (Google) Code Review
12 changed files with 580 additions and 3 deletions

View File

@@ -0,0 +1,71 @@
/*
* 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.codelab.page
import android.os.Bundle
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.api.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.ui.Footer
object FooterPageProvider : SettingsPageProvider {
override val name = "Footer"
@Composable
override fun Page(arguments: Bundle?) {
FooterPage()
}
@Composable
fun EntryItem() {
Preference(object : PreferenceModel {
override val title = "Sample Footer"
override val onClick = navigator(name)
})
}
}
@Composable
private fun FooterPage() {
Column(Modifier.verticalScroll(rememberScrollState())) {
Preference(remember {
object : PreferenceModel {
override val title = "Some Preference"
override val summary = stateOf("Some summary")
}
})
Footer(footerText = "Footer text always at the end of page.")
}
}
@Preview(showBackground = true)
@Composable
private fun FooterPagePreview() {
SettingsTheme {
FooterPage()
}
}

View File

@@ -50,10 +50,11 @@ private fun HomePage() {
)
PreferencePageProvider.EntryItem()
SwitchPreferencePageProvider.EntryItem()
ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = 0)
SliderPageProvider.EntryItem()
FooterPageProvider.EntryItem()
}
}

View File

@@ -21,6 +21,7 @@ import com.android.settingslib.spa.framework.api.SettingsPageRepository
object Destinations {
const val Home = "Home"
const val Preference = "Preference"
const val SwitchPreference = "SwitchPreference"
const val Argument = "Argument"
const val Slider = "Slider"
}
@@ -29,8 +30,10 @@ val codelabPageRepository = SettingsPageRepository(
allPages = listOf(
HomePageProvider,
PreferencePageProvider,
SwitchPreferencePageProvider,
ArgumentPageProvider,
SliderPageProvider,
FooterPageProvider,
),
startDestination = Destinations.Home,
)

View File

@@ -0,0 +1,129 @@
/*
* 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.codelab.page
import android.os.Bundle
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.api.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import kotlinx.coroutines.delay
object SwitchPreferencePageProvider : SettingsPageProvider {
override val name = Destinations.SwitchPreference
@Composable
override fun Page(arguments: Bundle?) {
SwitchPreferencePage()
}
@Composable
fun EntryItem() {
Preference(object : PreferenceModel {
override val title = "Sample SwitchPreference"
override val onClick = navigator(Destinations.SwitchPreference)
})
}
}
@Composable
private fun SwitchPreferencePage() {
Column(Modifier.verticalScroll(rememberScrollState())) {
SampleSwitchPreference()
SampleSwitchPreferenceWithSummary()
SampleSwitchPreferenceWithAsyncSummary()
SampleNotChangeableSwitchPreference()
}
}
@Composable
private fun SampleSwitchPreference() {
val checked = rememberSaveable { mutableStateOf(false) }
SwitchPreference(remember {
object : SwitchPreferenceModel {
override val title = "SwitchPreference"
override val checked = checked
override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked }
}
})
}
@Composable
private fun SampleSwitchPreferenceWithSummary() {
val checked = rememberSaveable { mutableStateOf(true) }
SwitchPreference(remember {
object : SwitchPreferenceModel {
override val title = "SwitchPreference"
override val summary = stateOf("With summary")
override val checked = checked
override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked }
}
})
}
@Composable
private fun SampleSwitchPreferenceWithAsyncSummary() {
val checked = rememberSaveable { mutableStateOf(true) }
val summary = produceState(initialValue = " ") {
delay(1000L)
value = "Async summary"
}
SwitchPreference(remember {
object : SwitchPreferenceModel {
override val title = "SwitchPreference"
override val summary = summary
override val checked = checked
override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked }
}
})
}
@Composable
private fun SampleNotChangeableSwitchPreference() {
val checked = rememberSaveable { mutableStateOf(true) }
SwitchPreference(remember {
object : SwitchPreferenceModel {
override val title = "SwitchPreference"
override val summary = stateOf("Not changeable")
override val changeable = stateOf(false)
override val checked = checked
override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked }
}
})
}
@Preview(showBackground = true)
@Composable
private fun SwitchPreferencePagePreview() {
SettingsTheme {
SwitchPreferencePage()
}
}

View File

@@ -31,6 +31,9 @@ android_library {
"androidx.navigation_navigation-compose",
"com.google.android.material_material",
],
kotlincflags: ["-Xjvm-default=all"],
kotlincflags: [
"-Xjvm-default=all",
"-Xopt-in=kotlin.RequiresOptIn",
],
min_sdk_version: "31",
}

View File

@@ -43,7 +43,7 @@ android {
}
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs = ["-Xjvm-default=all"]
freeCompilerArgs = ["-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn"]
}
buildFeatures {
compose true

View File

@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.dp
object SettingsDimension {
val itemIconSize = 24.dp
val itemIconContainerSize = 72.dp
val itemPaddingStart = 24.dp
val itemPaddingEnd = 16.dp

View File

@@ -0,0 +1,146 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.spa.widget.preference
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.selection.toggleable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.ui.SettingsSwitch
/**
* The widget model for [SwitchPreference] widget.
*/
interface SwitchPreferenceModel {
/**
* The title of this [SwitchPreference].
*/
val title: String
/**
* The summary of this [SwitchPreference].
*/
val summary: State<String>
get() = stateOf("")
/**
* Indicates whether this [SwitchPreference] is checked.
*
* This can be `null` during the data loading before the data is available.
*/
val checked: State<Boolean?>
/**
* Indicates whether this [SwitchPreference] is changeable.
*
* Not changeable [SwitchPreference] will be displayed in disabled style.
*/
val changeable: State<Boolean>
get() = stateOf(true)
/**
* The switch change handler of this [SwitchPreference].
*
* If `null`, this [SwitchPreference] is not [toggleable].
*/
val onCheckedChange: ((newChecked: Boolean) -> Unit)?
}
/**
* SwitchPreference widget.
*
* Data is provided through [SwitchPreferenceModel].
*/
@Composable
fun SwitchPreference(model: SwitchPreferenceModel) {
InternalSwitchPreference(
title = model.title,
summary = model.summary,
checked = model.checked,
changeable = model.changeable,
onCheckedChange = model.onCheckedChange,
)
}
@Composable
internal fun InternalSwitchPreference(
title: String,
summary: State<String> = "".toState(),
checked: State<Boolean?>,
changeable: State<Boolean> = true.toState(),
paddingStart: Dp = SettingsDimension.itemPaddingStart,
paddingEnd: Dp = SettingsDimension.itemPaddingEnd,
paddingVertical: Dp = SettingsDimension.itemPaddingVertical,
onCheckedChange: ((newChecked: Boolean) -> Unit)?,
) {
val checkedValue = checked.value
val indication = LocalIndication.current
val modifier = remember(checkedValue) {
if (checkedValue != null && onCheckedChange != null) {
Modifier.toggleable(
value = checkedValue,
interactionSource = MutableInteractionSource(),
indication = indication,
enabled = changeable.value,
role = Role.Switch,
onValueChange = onCheckedChange,
)
} else Modifier
}
BasePreference(
title = title,
summary = summary,
modifier = modifier,
enabled = changeable,
paddingStart = paddingStart,
paddingEnd = paddingEnd,
paddingVertical = paddingVertical,
) {
SettingsSwitch(checked = checked, changeable = changeable)
}
}
@Preview
@Composable
private fun SwitchPreferencePreview() {
SettingsTheme {
Column {
InternalSwitchPreference(
title = "Use Dark theme",
checked = true.toState(),
onCheckedChange = {},
)
InternalSwitchPreference(
title = "Use Dark theme",
summary = "Summary".toState(),
checked = false.toState(),
onCheckedChange = {},
)
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.framework.theme.SettingsTheme
@Composable
fun Footer(footerText: String) {
if (footerText.isEmpty()) return
Column(Modifier.padding(SettingsDimension.itemPadding)) {
SettingsIcon(imageVector = Icons.Outlined.Info, contentDescription = null)
Spacer(modifier = Modifier.height(SettingsDimension.itemPaddingVertical))
SettingsBody(footerText)
}
}
@Preview
@Composable
private fun FooterPreview() {
SettingsTheme {
Footer("Footer text always at the end of page.")
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.ui
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import com.android.settingslib.spa.framework.theme.SettingsDimension
@Composable
fun SettingsIcon(
imageVector: ImageVector,
contentDescription: String?,
) {
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
modifier = Modifier.size(SettingsDimension.itemIconSize),
tint = MaterialTheme.colorScheme.onSurface,
)
}

View File

@@ -0,0 +1,47 @@
/*
* 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.ui
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsSwitch(
checked: State<Boolean?>,
changeable: State<Boolean>,
onCheckedChange: ((newChecked: Boolean) -> Unit)? = null,
) {
// TODO: Replace Checkbox with Switch when the androidx.compose.material3_material3 library is
// updated to date.
val checkedValue = checked.value
if (checkedValue != null) {
Checkbox(
checked = checkedValue,
onCheckedChange = onCheckedChange,
enabled = changeable.value,
)
} else {
Checkbox(
checked = false,
onCheckedChange = null,
enabled = false,
)
}
}

View File

@@ -0,0 +1,91 @@
/*
* 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.preference
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.isToggleable
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.framework.compose.stateOf
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SwitchPreferenceTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun title_displayed() {
composeTestRule.setContent {
TestSwitchPreference(changeable = true)
}
composeTestRule.onNodeWithText("SwitchPreference").assertIsDisplayed()
}
@Test
fun toggleable_initialStateIsCorrect() {
composeTestRule.setContent {
TestSwitchPreference(changeable = true)
}
composeTestRule.onNode(isToggleable()).assertIsOff()
}
@Test
fun click_changeable_withEffect() {
composeTestRule.setContent {
TestSwitchPreference(changeable = true)
}
composeTestRule.onNodeWithText("SwitchPreference").performClick()
composeTestRule.onNode(isToggleable()).assertIsOn()
}
@Test
fun click_notChangeable_noEffect() {
composeTestRule.setContent {
TestSwitchPreference(changeable = false)
}
composeTestRule.onNodeWithText("SwitchPreference").performClick()
composeTestRule.onNode(isToggleable()).assertIsOff()
}
}
@Composable
private fun TestSwitchPreference(changeable: Boolean) {
val checked = rememberSaveable { mutableStateOf(false) }
SwitchPreference(remember {
object : SwitchPreferenceModel {
override val title = "SwitchPreference"
override val checked = checked
override val changeable = stateOf(changeable)
override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked }
}
})
}