Merge "Add ProgressBar and LoadingBar widgets to SPA and create gallery samples for them."

This commit is contained in:
Kelly Zhang
2022-10-21 01:50:27 +00:00
committed by Android (Google) Code Review
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()
}
}