From 49c3c064d7c86a659c25ed922a9e70f379d6b74f Mon Sep 17 00:00:00 2001 From: Florent Maitre Date: Fri, 29 Nov 2024 15:47:34 +0100 Subject: [PATCH] OudsButton now supports both determinate and indeterminate circular progress indicators --- NOTICE.txt | 2 - .../ui/components/button/ButtonDemoScreen.kt | 14 ++- .../ouds/core/component/button/OudsButton.kt | 91 +++++++++++++------ .../drawable/loading_indicator_circular.xml | 21 ----- 4 files changed, 73 insertions(+), 55 deletions(-) delete mode 100644 core/src/main/res/drawable/loading_indicator_circular.xml diff --git a/NOTICE.txt b/NOTICE.txt index 435b02d3..43ab807f 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -67,8 +67,6 @@ app/src/main/res/drawable-night-xxxhdpi/il_tokens_grid_min_width.png app/src/prod/res/drawable/ic_launcher_background.xml app/src/prod/res/drawable/ic_launcher_foreground.xml -core/src/main/res/drawable/loading_indicator_circular.xml - docs/images/favicon-16x16.png docs/images/orange-logo.svg diff --git a/app/src/main/java/com/orange/ouds/app/ui/components/button/ButtonDemoScreen.kt b/app/src/main/java/com/orange/ouds/app/ui/components/button/ButtonDemoScreen.kt index bdb4180a..6c212464 100644 --- a/app/src/main/java/com/orange/ouds/app/ui/components/button/ButtonDemoScreen.kt +++ b/app/src/main/java/com/orange/ouds/app/ui/components/button/ButtonDemoScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -43,12 +44,19 @@ fun ButtonDemoScreen() = DemoScreen(rememberButtonDemoState()) { selectedChipIndex = OudsButton.Hierarchy.entries.indexOf(hierarchy), onSelectionChange = { id -> hierarchy = OudsButton.Hierarchy.entries[id] } ) + val styles = remember { + listOf( + OudsButton.Style.Default, + OudsButton.Style.Loading(progress = null), + OudsButton.Style.Skeleton + ) + } CustomizationChoiceChipsColumn( modifier = Modifier.padding(top = OudsSpaceKeyToken.Fixed.Medium.value), label = stringResource(R.string.app_components_button_style_label), - chipsLabels = OudsButton.Style.entries.map { it.name }, - selectedChipIndex = OudsButton.Style.entries.indexOf(style), - onSelectionChange = { id -> style = OudsButton.Style.entries[id] } + chipsLabels = styles.map { it::class.simpleName.orEmpty() }, + selectedChipIndex = styles.indexOf(style), + onSelectionChange = { id -> style = styles[id] } ) CustomizationChoiceChipsColumn( modifier = Modifier.padding(top = OudsSpaceKeyToken.Fixed.Medium.value), diff --git a/core/src/main/java/com/orange/ouds/core/component/button/OudsButton.kt b/core/src/main/java/com/orange/ouds/core/component/button/OudsButton.kt index 6bd924ce..b238aa54 100644 --- a/core/src/main/java/com/orange/ouds/core/component/button/OudsButton.kt +++ b/core/src/main/java/com/orange/ouds/core/component/button/OudsButton.kt @@ -12,11 +12,7 @@ package com.orange.ouds.core.component.button -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween +import android.os.Parcelable import androidx.compose.foundation.border import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme @@ -33,8 +29,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -47,14 +43,14 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.orange.ouds.core.R import com.orange.ouds.core.component.content.OudsComponentContent import com.orange.ouds.core.component.content.OudsComponentIcon import com.orange.ouds.core.extensions.InteractionState @@ -69,6 +65,7 @@ import com.orange.ouds.theme.outerBorder import com.orange.ouds.theme.tokens.OudsBorderKeyToken import com.orange.ouds.theme.tokens.OudsColorKeyToken import com.orange.ouds.theme.tokens.OudsTypographyKeyToken +import kotlinx.parcelize.Parcelize /** * An OUDS button which displays only text. @@ -193,7 +190,7 @@ private fun OudsButton( modifier = modifier .widthIn(min = buttonTokens.sizeMinWidth.dp) .heightIn(min = buttonTokens.sizeMinHeight.dp, max = maxHeight) - .buttonBorder(hierarchy = hierarchy, state = state, shape = shape), + .border(hierarchy = hierarchy, state = state, shape = shape), enabled = state != OudsButton.State.Disabled && state != OudsButton.State.Loading && state != OudsButton.State.Skeleton, shape = shape, colors = buttonColors(hierarchy = hierarchy, buttonState = state), @@ -204,21 +201,9 @@ private fun OudsButton( Box(contentAlignment = Alignment.Center) { val isLoadingIndicatorVisible = state == OudsButton.State.Loading if (isLoadingIndicatorVisible) { - val infiniteTransition = rememberInfiniteTransition("LoadingIndicatorInfiniteTransition") - val angle by infiniteTransition.animateFloat( - initialValue = 0f, - targetValue = 360f, - animationSpec = infiniteRepeatable(tween(1000, easing = LinearEasing)), - label = "LoadingIndicatorAngleAnimation" - ) - Icon( - modifier = Modifier - .size(buttonTokens.sizeLoader.value) - .graphicsLayer { rotationZ = angle }, - contentDescription = null, - painter = painterResource(id = R.drawable.loading_indicator_circular), - tint = contentColor(hierarchy = hierarchy, state = state) - ) + val loadingStyle = style as? OudsButton.Style.Loading + val progress = if (LocalInspectionMode.current) 0.75f else loadingStyle?.progress + LoadingIndicator(hierarchy = hierarchy, progress) } Row( @@ -261,13 +246,13 @@ private fun rememberOudsButtonState( interactionState == InteractionState.Focused -> OudsButton.State.Focused else -> OudsButton.State.Enabled } - OudsButton.Style.Loading -> OudsButton.State.Loading + is OudsButton.Style.Loading -> OudsButton.State.Loading OudsButton.Style.Skeleton -> OudsButton.State.Skeleton } } @Composable -private fun Modifier.buttonBorder(hierarchy: OudsButton.Hierarchy, state: OudsButton.State, shape: Shape): Modifier { +private fun Modifier.border(hierarchy: OudsButton.Hierarchy, state: OudsButton.State, shape: Shape): Modifier { val borderWidth = borderWidth(hierarchy = hierarchy, state = state) val borderColor = borderColor(hierarchy = hierarchy, state = state) @@ -464,6 +449,33 @@ private fun contentPadding(icon: OudsButton.Icon?, text: String?): PaddingValues } } +@Composable +private fun LoadingIndicator(hierarchy: OudsButton.Hierarchy, progress: Float?) { + val modifier = Modifier.size(OudsTheme.componentsTokens.button.sizeLoader.value) + val color = contentColor(hierarchy = hierarchy, state = OudsButton.State.Loading) + val strokeWidth = 3.dp + val trackColor = Color.Transparent + val strokeCap = StrokeCap.Square + if (progress != null) { + CircularProgressIndicator( + progress = { progress }, + modifier = modifier, + color = color, + strokeWidth = strokeWidth, + trackColor = trackColor, + strokeCap = strokeCap + ) + } else { + CircularProgressIndicator( + modifier = modifier, + color = color, + strokeWidth = strokeWidth, + trackColor = trackColor, + strokeCap = strokeCap + ) + } +} + /** * Contains the default values used by OUDS buttons. */ @@ -532,10 +544,31 @@ object OudsButton { } /** - * Represents the style of an OUDS button. + * Represents the different styles of an OUDS button. */ - enum class Style { - Default, Loading, Skeleton + sealed class Style : Parcelable { + + /** + * The button displays an icon and/or a text and supports user interactions if it is enabled. + */ + @Parcelize + data object Default : Style() + + /** + * The button displays a circular loading indicator. + * + * @param progress The loading progress, where 0.0 represents no progress and 1.0 represents full progress. + * Values outside of this range are coerced into the range. + * Set this value to `null` to display a circular indeterminate progress indicator. + */ + @Parcelize + data class Loading(val progress: Float?) : Style() + + /** + * This button displays a skeleton. + */ + @Parcelize + data object Skeleton : Style() } internal enum class State { diff --git a/core/src/main/res/drawable/loading_indicator_circular.xml b/core/src/main/res/drawable/loading_indicator_circular.xml deleted file mode 100644 index 91913144..00000000 --- a/core/src/main/res/drawable/loading_indicator_circular.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - -