Add the slider widget to SPA and create samples in codelab slider page.
Test: manually test on device.
Bug: 241857459
Change-Id: I5cb252cfb82d7258a56bb84a7311b2ec3a13cdc1
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 27c70e4..b07b180 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
@@ -52,6 +52,8 @@
PreferencePageProvider.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 862fc1e..86b6843 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
@@ -22,9 +22,15 @@
const val Home = "Home"
const val Preference = "Preference"
const val Argument = "Argument"
+ const val Slider = "Slider"
}
val codelabPageRepository = SettingsPageRepository(
- allPages = listOf(HomePageProvider, PreferencePageProvider, ArgumentPageProvider),
+ allPages = listOf(
+ HomePageProvider,
+ PreferencePageProvider,
+ ArgumentPageProvider,
+ SliderPageProvider,
+ ),
startDestination = Destinations.Home,
)
diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SliderPage.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SliderPage.kt
new file mode 100644
index 0000000..67d53b7
--- /dev/null
+++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SliderPage.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.material.icons.Icons
+import androidx.compose.material.icons.outlined.AccessAlarm
+import androidx.compose.material.icons.outlined.MusicNote
+import androidx.compose.material.icons.outlined.MusicOff
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.api.SettingsPageProvider
+import com.android.settingslib.spa.framework.navigator
+import com.android.settingslib.spa.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.slider.SettingsSlider
+import com.android.settingslib.spa.widget.slider.SettingsSliderModel
+
+object SliderPageProvider : SettingsPageProvider {
+ override val name = Destinations.Slider
+
+ @Composable
+ override fun Page(arguments: Bundle?) {
+ SliderPage()
+ }
+
+ @Composable
+ fun EntryItem() {
+ Preference(object : PreferenceModel {
+ override val title = "Sample Slider"
+ override val onClick = navigator(Destinations.Slider)
+ })
+ }
+}
+
+@Composable
+private fun SliderPage() {
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ SettingsSlider(object : SettingsSliderModel {
+ override val title = "Slider"
+ override val initValue = 40
+ })
+
+ SettingsSlider(object : SettingsSliderModel {
+ override val title = "Slider with icon"
+ override val initValue = 30
+ override val onValueChangeFinished = {
+ println("onValueChangeFinished")
+ }
+ override val icon = Icons.Outlined.AccessAlarm
+ })
+
+ val initValue = 0
+ var icon by remember { mutableStateOf(Icons.Outlined.MusicOff) }
+ var sliderPosition by remember { mutableStateOf(initValue) }
+ SettingsSlider(object : SettingsSliderModel {
+ override val title = "Slider with changeable icon"
+ override val initValue = initValue
+ override val onValueChange = { it: Int ->
+ sliderPosition = it
+ icon = if (it > 0) Icons.Outlined.MusicNote else Icons.Outlined.MusicOff
+ }
+ override val onValueChangeFinished = {
+ println("onValueChangeFinished: the value is $sliderPosition")
+ }
+ override val icon = icon
+ })
+
+ SettingsSlider(object : SettingsSliderModel {
+ override val title = "Slider with steps"
+ override val initValue = 2
+ override val valueRange = 1..5
+ override val showSteps = true
+ })
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SliderPagePreview() {
+ SettingsTheme {
+ SliderPage()
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/slider/SettingsSlider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/slider/SettingsSlider.kt
new file mode 100644
index 0000000..a69244a
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/slider/SettingsSlider.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.slider
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.AccessAlarm
+import androidx.compose.material.icons.outlined.MusicNote
+import androidx.compose.material.icons.outlined.MusicOff
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Slider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.BaseLayout
+import kotlin.math.roundToInt
+
+/**
+ * The widget model for [SettingsSlider] widget.
+ */
+interface SettingsSliderModel {
+ /**
+ * The title of this [SettingsSlider].
+ */
+ val title: String
+
+ /**
+ * The initial position of the [SettingsSlider].
+ */
+ val initValue: Int
+
+ /**
+ * The value range for this [SettingsSlider].
+ */
+ val valueRange: IntRange
+ get() = 0..100
+
+ /**
+ * The lambda to be invoked during the value change by dragging or a click. This callback is
+ * used to get the real time value of the [SettingsSlider].
+ */
+ val onValueChange: ((value: Int) -> Unit)?
+ get() = null
+
+ /**
+ * The lambda to be invoked when value change has ended. This callback is used to get when the
+ * user has completed selecting a new value by ending a drag or a click.
+ */
+ val onValueChangeFinished: (() -> Unit)?
+ get() = null
+
+ /**
+ * The icon image for [SettingsSlider]. If not specified, the slider hides the icon by default.
+ */
+ val icon: ImageVector?
+ get() = null
+
+ /**
+ * Indicates whether to show step marks. If show step marks, when user finish sliding,
+ * the slider will automatically jump to the nearest step mark. Otherwise, the slider hides
+ * the step marks by default.
+ *
+ * The step is fixed to 1.
+ */
+ val showSteps: Boolean
+ get() = false
+}
+
+/**
+ * Settings slider widget.
+ *
+ * Data is provided through [SettingsSliderModel].
+ */
+@Composable
+fun SettingsSlider(model: SettingsSliderModel) {
+ SettingsSlider(
+ title = model.title,
+ initValue = model.initValue,
+ valueRange = model.valueRange,
+ onValueChange = model.onValueChange,
+ onValueChangeFinished = model.onValueChangeFinished,
+ icon = model.icon,
+ showSteps = model.showSteps,
+ )
+}
+
+@Composable
+internal fun SettingsSlider(
+ title: String,
+ initValue: Int,
+ valueRange: IntRange = 0..100,
+ onValueChange: ((value: Int) -> Unit)? = null,
+ onValueChangeFinished: (() -> Unit)? = null,
+ icon: ImageVector? = null,
+ showSteps: Boolean = false,
+ modifier: Modifier = Modifier,
+) {
+ var sliderPosition by rememberSaveable { mutableStateOf(initValue.toFloat()) }
+ BaseLayout(
+ title = title,
+ subTitle = {
+ Slider(
+ value = sliderPosition,
+ onValueChange = {
+ sliderPosition = it
+ onValueChange?.invoke(sliderPosition.roundToInt())
+ },
+ modifier = modifier,
+ valueRange = valueRange.first.toFloat()..valueRange.last.toFloat(),
+ steps = if (showSteps) (valueRange.count() - 2) else 0,
+ onValueChangeFinished = onValueChangeFinished,
+ )
+ },
+ icon = if (icon != null) ({
+ Icon(imageVector = icon, contentDescription = null)
+ }) else null,
+ )
+}
+
+@Preview
+@Composable
+private fun SettingsSliderPreview() {
+ SettingsTheme {
+ val initValue = 30
+ var sliderPosition by rememberSaveable { mutableStateOf(initValue) }
+ SettingsSlider(
+ title = "Alarm Volume",
+ initValue = 30,
+ onValueChange = { sliderPosition = it },
+ onValueChangeFinished = {
+ println("onValueChangeFinished: the value is $sliderPosition")
+ },
+ icon = Icons.Outlined.AccessAlarm,
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun SettingsSliderIconChangePreview() {
+ SettingsTheme {
+ var icon by remember { mutableStateOf(Icons.Outlined.MusicNote) }
+ SettingsSlider(
+ title = "Media Volume",
+ initValue = 40,
+ onValueChange = { it: Int ->
+ icon = if (it > 0) Icons.Outlined.MusicNote else Icons.Outlined.MusicOff
+ },
+ icon = icon,
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun SettingsSliderStepsPreview() {
+ SettingsTheme {
+ SettingsSlider(
+ title = "Display Text",
+ initValue = 2,
+ valueRange = 1..5,
+ showSteps = true,
+ )
+ }
+}