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
This commit is contained in:
Kelly
2022-10-19 09:40:07 +08:00
parent 94cc29a128
commit 8391a3f08a
8 changed files with 541 additions and 1 deletions

View File

@@ -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(),

View File

@@ -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(),
)
}

View File

@@ -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)
}
}

View File

@@ -72,7 +72,7 @@ internal fun BaseLayout(
}
@Composable
private fun BaseIcon(
internal fun BaseIcon(
icon: @Composable (() -> Unit)?,
modifier: Modifier,
paddingStart: Dp,

View File

@@ -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()
}
}

View File

@@ -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()
}
}
}

View File

@@ -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)
)
}
}

View File

@@ -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()
}
}