From 8391a3f08a965ee421ed21b789b731f6adf2a2de Mon Sep 17 00:00:00 2001 From: Kelly Date: Wed, 19 Oct 2022 09:40:07 +0800 Subject: [PATCH] Add ProgressBar and LoadingBar widgets to SPA and create gallery samples for them. The loading bar is indeterminate, and expresses the loading status or waiting time. The progress bar is reflecting the progress of a process, and need a progress fraction input. Test: Unit test and manually test on device Bug: 254364764 Change-Id: I7faf20beab1e46c5c9d4a9ab07a916a7f506412c --- .../spa/gallery/GallerySpaEnvironment.kt | 2 + .../settingslib/spa/gallery/home/HomePage.kt | 2 + .../spa/gallery/page/ProgressBarPage.kt | 134 ++++++++++++++ .../spa/widget/preference/BaseLayout.kt | 2 +- .../preference/ProgressBarPreference.kt | 171 ++++++++++++++++++ .../settingslib/spa/widget/ui/LoadingBar.kt | 59 ++++++ .../settingslib/spa/widget/ui/ProgressBar.kt | 98 ++++++++++ .../preference/ProgressBarPreferenceTest.kt | 74 ++++++++ 8 files changed, 541 insertions(+), 1 deletion(-) create mode 100644 packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt create mode 100644 packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index acb22dac9854b..4af25893ea37f 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -25,6 +25,7 @@ import com.android.settingslib.spa.gallery.home.HomePageProvider import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.FooterPageProvider import com.android.settingslib.spa.gallery.page.IllustrationPageProvider +import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider import com.android.settingslib.spa.gallery.page.SliderPageProvider import com.android.settingslib.spa.gallery.preference.MainSwitchPreferencePageProvider @@ -66,6 +67,7 @@ object GallerySpaEnvironment : SpaEnvironment() { IllustrationPageProvider, CategoryPageProvider, ActionButtonPageProvider, + ProgressBarPageProvider, ), rootPages = listOf( HomePageProvider.createSettingsPage(), 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 e40775a958134..7fd49db937483 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 @@ -31,6 +31,7 @@ import com.android.settingslib.spa.gallery.page.ArgumentPageModel import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.FooterPageProvider import com.android.settingslib.spa.gallery.page.IllustrationPageProvider +import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider import com.android.settingslib.spa.gallery.page.SliderPageProvider import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider @@ -54,6 +55,7 @@ object HomePageProvider : SettingsPageProvider { IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ) } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt new file mode 100644 index 0000000000000..dc45df4a0374d --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt @@ -0,0 +1,134 @@ +/* + * 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.page + +import android.os.Bundle +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.SystemUpdate +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import com.android.settingslib.spa.framework.common.SettingsPage +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.preference.ProgressBarPreference +import com.android.settingslib.spa.widget.preference.ProgressBarPreferenceModel +import com.android.settingslib.spa.widget.preference.ProgressBarWithDataPreference +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.CircularLoadingBar +import com.android.settingslib.spa.widget.ui.CircularProgressBar +import com.android.settingslib.spa.widget.ui.LinearLoadingBar +import kotlinx.coroutines.delay + +private const val TITLE = "Sample ProgressBar" + +object ProgressBarPageProvider : SettingsPageProvider { + override val name = "ProgressBar" + + fun buildInjectEntry(): SettingsEntryBuilder { + return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name)) + .setIsAllowSearch(true) + .setUiLayoutFn { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } + } + + @Composable + override fun Page(arguments: Bundle?) { + // Mocks a loading time of 2 seconds. + var loading by remember { mutableStateOf(true) } + LaunchedEffect(Unit) { + delay(2000) + loading = false + } + + RegularScaffold(title = TITLE) { + // Auto update the progress and finally jump tp 0.4f. + var progress by remember { mutableStateOf(0f) } + LaunchedEffect(Unit) { + delay(2000) + while (progress < 1f) { + delay(100) + progress += 0.01f + } + delay(500) + progress = 0.4f + } + + // Show as a placeholder for progress bar + LargeProgressBar(progress) + // The remaining information only shows after loading complete. + if (!loading) { + SimpleProgressBar() + ProgressBarWithData() + CircularProgressBar(progress = progress, radius = 160f) + } + } + + // Add loading bar examples, running for 2 seconds. + LinearLoadingBar(isLoading = loading, yOffset = 64.dp) + CircularLoadingBar(isLoading = loading) + } +} + +@Composable +private fun LargeProgressBar(progress: Float) { + ProgressBarPreference(object : ProgressBarPreferenceModel { + override val title = "Large Progress Bar" + override val progress = progress + override val height = 20f + }) +} + +@Composable +private fun SimpleProgressBar() { + ProgressBarPreference(object : ProgressBarPreferenceModel { + override val title = "Simple Progress Bar" + override val progress = 0.2f + override val icon = Icons.Outlined.SystemUpdate + }) +} + +@Composable +private fun ProgressBarWithData() { + ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel { + override val title = "Progress Bar with Data" + override val progress = 0.2f + override val icon = Icons.Outlined.Delete + }, data = "25G") +} + +@Preview(showBackground = true) +@Composable +private fun ProgressBarPagePreview() { + SettingsTheme { + ProgressBarPageProvider.Page(null) + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt index 9a34dbf367359..6135203ec703b 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt @@ -72,7 +72,7 @@ internal fun BaseLayout( } @Composable -private fun BaseIcon( +internal fun BaseIcon( icon: @Composable (() -> Unit)?, modifier: Modifier, paddingStart: Dp, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt new file mode 100644 index 0000000000000..b8c59ad07cfd9 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt @@ -0,0 +1,171 @@ +/* + * 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.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.widget.ui.LinearProgressBar +import com.android.settingslib.spa.widget.ui.SettingsTitle + +/** + * The widget model for [ProgressBarPreference] widget. + */ +interface ProgressBarPreferenceModel { + /** + * The title of this [ProgressBarPreference]. + */ + val title: String + + /** + * The progress fraction of the ProgressBar. Should be float in range [0f, 1f] + */ + val progress: Float + + /** + * The icon image for [ProgressBarPreference]. If not specified, hides the icon by default. + */ + val icon: ImageVector? + get() = null + + /** + * The height of the ProgressBar. + */ + val height: Float + get() = 4f + + /** + * Indicates whether to use rounded corner for the progress bars. + */ + val roundedCorner: Boolean + get() = true +} + +/** + * Progress bar preference widget. + * + * Data is provided through [ProgressBarPreferenceModel]. + */ +@Composable +fun ProgressBarPreference(model: ProgressBarPreferenceModel) { + ProgressBarPreference( + title = model.title, + progress = model.progress, + icon = model.icon, + height = model.height, + roundedCorner = model.roundedCorner, + ) +} + +/** + * Progress bar with data preference widget. + */ +@Composable +fun ProgressBarWithDataPreference(model: ProgressBarPreferenceModel, data: String) { + val icon = model.icon + ProgressBarWithDataPreference( + title = model.title, + data = data, + progress = model.progress, + icon = if (icon != null) ({ + Icon(imageVector = icon, contentDescription = null) + }) else null, + height = model.height, + roundedCorner = model.roundedCorner, + ) +} + +@Composable +internal fun ProgressBarPreference( + title: String, + progress: Float, + icon: ImageVector? = null, + height: Float = 4f, + roundedCorner: Boolean = true, +) { + BaseLayout( + title = title, + subTitle = { + LinearProgressBar(progress, height, roundedCorner) + }, + icon = if (icon != null) ({ + Icon(imageVector = icon, contentDescription = null) + }) else null, + ) +} + + +@Composable +internal fun ProgressBarWithDataPreference( + title: String, + data: String, + progress: Float, + icon: (@Composable () -> Unit)? = null, + height: Float = 4f, + roundedCorner: Boolean = true, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = SettingsDimension.itemPaddingEnd), + verticalAlignment = Alignment.CenterVertically, + ) { + BaseIcon(icon, Modifier, SettingsDimension.itemPaddingStart) + TitleWithData( + title = title, + data = data, + subTitle = { + LinearProgressBar(progress, height, roundedCorner) + }, + modifier = Modifier + .weight(1f) + .padding(vertical = SettingsDimension.itemPaddingVertical), + ) + } +} + +@Composable +private fun TitleWithData( + title: String, + data: String, + subTitle: @Composable () -> Unit, + modifier: Modifier +) { + Column(modifier) { + Row { + Box(modifier = Modifier.weight(1f)) { + SettingsTitle(title) + } + Text( + text = data, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.titleMedium, + ) + } + subTitle() + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt new file mode 100644 index 0000000000000..1741f134f3d16 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt @@ -0,0 +1,59 @@ +/* + * 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.Box +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Indeterminate linear progress bar. Expresses an unspecified wait time. + */ +@Composable +fun LinearLoadingBar( + isLoading: Boolean, + xOffset: Dp = 0.dp, + yOffset: Dp = 0.dp +) { + if (isLoading) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .absoluteOffset(xOffset, yOffset) + ) + } +} + +/** + * Indeterminate circular progress bar. Expresses an unspecified wait time. + */ +@Composable +fun CircularLoadingBar(isLoading: Boolean) { + if (isLoading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt new file mode 100644 index 0000000000000..5d8502db49864 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt @@ -0,0 +1,98 @@ +/* + * 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.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.progressSemantics +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.dp + +/** + * Determinate linear progress bar. Displays the current progress of the whole process. + * + * Rounded corner is supported and enabled by default. + */ +@Composable +fun LinearProgressBar( + progress: Float, + height: Float = 4f, + roundedCorner: Boolean = true +) { + Box(modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)) { + val color = MaterialTheme.colorScheme.onSurface + val trackColor = MaterialTheme.colorScheme.surfaceVariant + Canvas( + Modifier + .progressSemantics(progress) + .fillMaxWidth() + .height(height.dp) + ) { + drawLinearBarTrack(trackColor, roundedCorner) + drawLinearBar(progress, color, roundedCorner) + } + } +} + +private fun DrawScope.drawLinearBar( + endFraction: Float, + color: Color, + roundedCorner: Boolean +) { + val width = endFraction * size.width + drawRoundRect( + color = color, + size = Size(width, size.height), + cornerRadius = if (roundedCorner) CornerRadius( + size.height / 2, + size.height / 2 + ) else CornerRadius.Zero, + ) +} + +private fun DrawScope.drawLinearBarTrack( + color: Color, + roundedCorner: Boolean +) = drawLinearBar(1f, color, roundedCorner) + +/** + * Determinate circular progress bar. Displays the current progress of the whole process. + * + * Displayed in default material3 style, and rounded corner is not supported. + */ +@Composable +fun CircularProgressBar(progress: Float, radius: Float = 40f) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator( + progress = progress, + modifier = Modifier.size(radius.dp, radius.dp) + ) + } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt new file mode 100644 index 0000000000000..5611f8ce14dbb --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt @@ -0,0 +1,74 @@ +/* + * 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.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.semantics.SemanticsProperties.ProgressBarRangeInfo +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ProgressBarPreferenceTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun title_displayed() { + composeTestRule.setContent { + ProgressBarPreference(object : ProgressBarPreferenceModel { + override val title = "Title" + override val progress = 0.2f + }) + } + composeTestRule.onNodeWithText("Title").assertIsDisplayed() + } + + @Test + fun data_displayed() { + composeTestRule.setContent { + ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel { + override val title = "Title" + override val progress = 0.2f + }, data = "Data") + } + composeTestRule.onNodeWithText("Title").assertIsDisplayed() + composeTestRule.onNodeWithText("Data").assertIsDisplayed() + } + + @Test + fun progressBar_displayed() { + composeTestRule.setContent { + ProgressBarPreference(object : ProgressBarPreferenceModel { + override val title = "Title" + override val progress = 0.2f + }) + } + + fun progressEqualsTo(progress: Float): SemanticsMatcher = + SemanticsMatcher.expectValue( + ProgressBarRangeInfo, + ProgressBarRangeInfo(progress, 0f..1f, 0) + ) + composeTestRule.onNode(progressEqualsTo(0.2f)).assertIsDisplayed() + } +}