diff options
8 files changed, 295 insertions, 0 deletions
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt index e300624bcc95..3f375345f30f 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt @@ -24,6 +24,7 @@ import com.android.settingslib.spa.gallery.page.PreferencePageProvider import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider import com.android.settingslib.spa.gallery.page.SliderPageProvider import com.android.settingslib.spa.gallery.page.SwitchPreferencePageProvider +import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider val galleryPageProviders = SettingsPageProviderRepository( allPagesList = listOf( @@ -32,6 +33,7 @@ val galleryPageProviders = SettingsPageProviderRepository( SwitchPreferencePageProvider, ArgumentPageProvider, SliderPageProvider, + SpinnerPageProvider, SettingsPagerPageProvider, FooterPageProvider, ), diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt index a85ee2a2a309..089920c8e8cf 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt @@ -29,6 +29,7 @@ import com.android.settingslib.spa.gallery.page.PreferencePageProvider import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider import com.android.settingslib.spa.gallery.page.SliderPageProvider import com.android.settingslib.spa.gallery.page.SwitchPreferencePageProvider +import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider import com.android.settingslib.spa.widget.scaffold.HomeScaffold object HomePageProvider : SettingsPageProvider { @@ -48,6 +49,7 @@ private fun HomePage() { ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = 0) SliderPageProvider.EntryItem() + SpinnerPageProvider.EntryItem() SettingsPagerPageProvider.EntryItem() FooterPageProvider.EntryItem() } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt new file mode 100644 index 000000000000..7efa85b4ad78 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt @@ -0,0 +1,75 @@ +/* + * 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.gallery.ui + +import android.os.Bundle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +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.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Spinner + +private const val TITLE = "Sample Spinner" + +object SpinnerPageProvider : SettingsPageProvider { + override val name = "Spinner" + + @Composable + override fun Page(arguments: Bundle?) { + SpinnerPage() + } + + @Composable + fun EntryItem() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } +} + +@Composable +private fun SpinnerPage() { + RegularScaffold(title = TITLE) { + val selectedIndex = rememberSaveable { mutableStateOf(0) } + Spinner( + options = (1..3).map { "Option $it" }, + selectedIndex = selectedIndex.value, + setIndex = { selectedIndex.value = it }, + ) + Preference(object : PreferenceModel { + override val title = "Selected index" + override val summary = remember { derivedStateOf { selectedIndex.value.toString() } } + }) + } +} + +@Preview(showBackground = true) +@Composable +private fun SpinnerPagePreview() { + SettingsTheme { + SpinnerPage() + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt index 27fdc916a434..bc316f71cf23 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt @@ -31,6 +31,10 @@ data class SettingsColorScheme( val secondaryText: Color = Color.Unspecified, val primaryContainer: Color = Color.Unspecified, val onPrimaryContainer: Color = Color.Unspecified, + val spinnerHeaderContainer: Color = Color.Unspecified, + val onSpinnerHeaderContainer: Color = Color.Unspecified, + val spinnerItemContainer: Color = Color.Unspecified, + val onSpinnerItemContainer: Color = Color.Unspecified, ) internal val LocalColorScheme = staticCompositionLocalOf { SettingsColorScheme() } @@ -65,6 +69,10 @@ private fun dynamicLightColorScheme(context: Context): SettingsColorScheme { secondaryText = tonalPalette.neutralVariant30, primaryContainer = tonalPalette.primary90, onPrimaryContainer = tonalPalette.neutral10, + spinnerHeaderContainer = tonalPalette.primary90, + onSpinnerHeaderContainer = tonalPalette.neutral10, + spinnerItemContainer = tonalPalette.secondary90, + onSpinnerItemContainer = tonalPalette.neutralVariant30, ) } @@ -87,5 +95,9 @@ private fun dynamicDarkColorScheme(context: Context): SettingsColorScheme { secondaryText = tonalPalette.neutralVariant80, primaryContainer = tonalPalette.secondary90, onPrimaryContainer = tonalPalette.neutral10, + spinnerHeaderContainer = tonalPalette.primary90, + onSpinnerHeaderContainer = tonalPalette.neutral10, + spinnerItemContainer = tonalPalette.secondary90, + onSpinnerItemContainer = tonalPalette.neutralVariant30, ) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt new file mode 100644 index 000000000000..429b81a04659 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt @@ -0,0 +1,131 @@ +/* + * 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.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowDropDown +import androidx.compose.material.icons.outlined.ArrowDropUp +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsTheme + +@Composable +fun Spinner(options: List<String>, selectedIndex: Int, setIndex: (index: Int) -> Unit) { + if (options.isEmpty()) { + return + } + + var expanded by rememberSaveable { mutableStateOf(false) } + + Box( + modifier = Modifier + .padding(SettingsDimension.itemPadding) + .selectableGroup(), + ) { + val contentPadding = PaddingValues(horizontal = SettingsDimension.itemPaddingEnd) + Button( + onClick = { expanded = true }, + modifier = Modifier.height(36.dp), + colors = ButtonDefaults.buttonColors( + containerColor = SettingsTheme.colorScheme.spinnerHeaderContainer, + contentColor = SettingsTheme.colorScheme.onSpinnerHeaderContainer, + ), + contentPadding = contentPadding, + ) { + SpinnerText(options[selectedIndex]) + Icon( + imageVector = when { + expanded -> Icons.Outlined.ArrowDropUp + else -> Icons.Outlined.ArrowDropDown + }, + contentDescription = null, + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.background(SettingsTheme.colorScheme.spinnerItemContainer), + offset = DpOffset(x = 0.dp, y = 4.dp), + ) { + options.forEachIndexed { index, option -> + DropdownMenuItem( + text = { + SpinnerText( + text = option, + modifier = Modifier.padding(end = 24.dp), + color = SettingsTheme.colorScheme.onSpinnerItemContainer, + ) + }, + onClick = { + expanded = false + setIndex(index) + }, + contentPadding = contentPadding, + ) + } + } + } +} + +@Composable +private fun SpinnerText( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, +) { + Text( + text = text, + modifier = modifier.padding(end = SettingsDimension.itemPaddingEnd), + color = color, + style = MaterialTheme.typography.labelLarge, + ) +} + +@Preview(showBackground = true) +@Composable +private fun SpinnerPreview() { + SettingsTheme { + var selectedIndex by rememberSaveable { mutableStateOf(0) } + Spinner( + options = (1..3).map { "Option $it" }, + selectedIndex = selectedIndex, + setIndex = { selectedIndex = it }, + ) + } +} diff --git a/packages/SettingsLib/Spa/tests/Android.bp b/packages/SettingsLib/Spa/tests/Android.bp index 037d8c4e78b1..1ce49fa520b9 100644 --- a/packages/SettingsLib/Spa/tests/Android.bp +++ b/packages/SettingsLib/Spa/tests/Android.bp @@ -31,6 +31,7 @@ android_test { "androidx.compose.runtime_runtime", "androidx.compose.ui_ui-test-junit4", "androidx.compose.ui_ui-test-manifest", + "truth-prebuilt", ], kotlincflags: ["-Xjvm-default=all"], } diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle index be5a5ec40c4f..5f93a9f4e4b5 100644 --- a/packages/SettingsLib/Spa/tests/build.gradle +++ b/packages/SettingsLib/Spa/tests/build.gradle @@ -63,5 +63,6 @@ dependencies { androidTestImplementation(project(":spa")) androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' androidTestImplementation("androidx.compose.ui:ui-test-junit4:$jetpack_compose_version") + androidTestImplementation 'com.google.truth:truth:1.1.3' androidTestDebugImplementation "androidx.compose.ui:ui-test-manifest:$jetpack_compose_version" } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/SpinnerTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/SpinnerTest.kt new file mode 100644 index 000000000000..6c56d63c18c7 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/SpinnerTest.kt @@ -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.widget.ui + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.assertIsDisplayed +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.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SpinnerTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun spinner_initialState() { + var selectedIndex by mutableStateOf(0) + composeTestRule.setContent { + Spinner( + options = (1..3).map { "Option $it" }, + selectedIndex = selectedIndex, + setIndex = { selectedIndex = it }, + ) + } + + composeTestRule.onNodeWithText("Option 1").assertIsDisplayed() + composeTestRule.onNodeWithText("Option 2").assertDoesNotExist() + assertThat(selectedIndex).isEqualTo(0) + } + + @Test + fun spinner_canChangeState() { + var selectedIndex by mutableStateOf(0) + composeTestRule.setContent { + Spinner( + options = (1..3).map { "Option $it" }, + selectedIndex = selectedIndex, + setIndex = { selectedIndex = it }, + ) + } + + composeTestRule.onNodeWithText("Option 1").performClick() + composeTestRule.onNodeWithText("Option 2").performClick() + + composeTestRule.onNodeWithText("Option 1").assertDoesNotExist() + composeTestRule.onNodeWithText("Option 2").assertIsDisplayed() + assertThat(selectedIndex).isEqualTo(1) + } +} |