diff options
8 files changed, 421 insertions, 3 deletions
diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/HomePage.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/HomePage.kt index 171a16118052..9e9341316f23 100644 --- a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/HomePage.kt +++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/HomePage.kt @@ -50,7 +50,7 @@ private fun HomePage() { ) PreferencePageProvider.EntryItem() - + SwitchPreferencePageProvider.EntryItem() ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = 0) SliderPageProvider.EntryItem() diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/PageRepository.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/PageRepository.kt index c24541a903da..b152d697552a 100644 --- a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/PageRepository.kt +++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/PageRepository.kt @@ -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, ), diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SwitchPreferencePage.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SwitchPreferencePage.kt new file mode 100644 index 000000000000..b566afa84d42 --- /dev/null +++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SwitchPreferencePage.kt @@ -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() + } +} diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp index 463c0765943f..3c8d91edc05b 100644 --- a/packages/SettingsLib/Spa/spa/Android.bp +++ b/packages/SettingsLib/Spa/spa/Android.bp @@ -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", } diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index 60794c88d00d..49b5e2e84667 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -43,7 +43,7 @@ android { } kotlinOptions { jvmTarget = '1.8' - freeCompilerArgs = ["-Xjvm-default=all"] + freeCompilerArgs = ["-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn"] } buildFeatures { compose true diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt new file mode 100644 index 000000000000..0dab0dfe1d6d --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt @@ -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 = {}, + ) + } + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt new file mode 100644 index 000000000000..45d5f6baa9cb --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt @@ -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, + ) + } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SwitchPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SwitchPreferenceTest.kt new file mode 100644 index 000000000000..d6c8fbc9dc9e --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SwitchPreferenceTest.kt @@ -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 } + } + }) +} |