diff options
8 files changed, 541 insertions, 1 deletions
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 acb22dac9854..4af25893ea37 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 e40775a95813..7fd49db93748 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 000000000000..dc45df4a0374 --- /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 9a34dbf36735..6135203ec703 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 000000000000..b8c59ad07cfd --- /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 000000000000..1741f134f3d1 --- /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 000000000000..5d8502db4986 --- /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 000000000000..5611f8ce14db --- /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() + } +} |