Add SwitchPreference for SpaLib

Temporarily use material3's Checkbox instead of Switch, before the
androidx.compose.material3_material3 library is updated to date.

Bug: 235727273
Test: Unit test & Manual with Codelab App
Change-Id: I3121f9a389ff29f88b2cf2c4171dceee1aa03dbe
This commit is contained in:
Chaohui Wang
2022-08-12 14:55:29 +08:00
parent 9244fef1b2
commit 573398eddf
8 changed files with 421 additions and 3 deletions

View File

@@ -50,7 +50,7 @@ private fun HomePage() {
)
PreferencePageProvider.EntryItem()
SwitchPreferencePageProvider.EntryItem()
ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = 0)
SliderPageProvider.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,6 +30,7 @@ val codelabPageRepository = SettingsPageRepository(
allPages = listOf(
HomePageProvider,
PreferencePageProvider,
SwitchPreferencePageProvider,
ArgumentPageProvider,
SliderPageProvider,
),

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

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