diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/buttons/IconButtonsExamples.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/buttons/IconButtonsExamples.kt index 97f4c6c56..b4aab43a3 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/buttons/IconButtonsExamples.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/buttons/IconButtonsExamples.kt @@ -24,9 +24,11 @@ package com.adevinta.spark.catalog.examples.samples.buttons import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column 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.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.unit.dp import com.adevinta.spark.catalog.model.Example @@ -36,9 +38,12 @@ import com.adevinta.spark.components.iconbuttons.IconButtonFilled import com.adevinta.spark.components.iconbuttons.IconButtonGhost import com.adevinta.spark.components.iconbuttons.IconButtonOutlined import com.adevinta.spark.components.iconbuttons.IconButtonTinted -import com.adevinta.spark.icons.LikeFill +import com.adevinta.spark.icons.BellShake +import com.adevinta.spark.icons.SparkAnimatedIcons import com.adevinta.spark.icons.SparkIcon -import com.adevinta.spark.icons.SparkIcons +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds private const val IconButtonsExampleDescription = "Icon Button examples" private const val IconButtonsExampleSourceUrl = "$SampleSourceUrl/IconButtonSamples.kt" @@ -56,6 +61,7 @@ public val IconButtonsExamples: List = listOf( icon = icon, contentDescription = contentDescription, isLoading = isLoading, + atEnd = false, ) }, ) @@ -143,7 +149,7 @@ private fun IconButtonSample( Column( verticalArrangement = Arrangement.spacedBy(16.dp), ) { - val icon = SparkIcons.LikeFill + val icon = SparkAnimatedIcons.BellShake val contentDescription = "Localized Content Description" val isLoading by remember { mutableStateOf(false) } button( diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/icons/IconsExamples.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/icons/IconsExamples.kt new file mode 100644 index 000000000..a442c9b29 --- /dev/null +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/examples/samples/icons/IconsExamples.kt @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2023 Adevinta + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.adevinta.spark.catalog.examples.samples.icons + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastFirst +import com.adevinta.spark.SparkTheme +import com.adevinta.spark.catalog.model.Example +import com.adevinta.spark.catalog.util.SampleSourceUrl +import com.adevinta.spark.components.icons.Icon +import com.adevinta.spark.components.surface.Surface +import com.adevinta.spark.components.text.Text +import com.adevinta.spark.icons.AccountToFill +import com.adevinta.spark.icons.AccountToFillAlt +import com.adevinta.spark.icons.AddToFill +import com.adevinta.spark.icons.LikeToFill +import com.adevinta.spark.icons.MessageToOutline +import com.adevinta.spark.icons.SearchToOutline +import com.adevinta.spark.icons.SparkAnimatedIcons +import com.adevinta.spark.tokens.disabled +import com.adevinta.spark.tokens.highlight +import kotlin.math.roundToInt + +private const val IconsExampleSourceUrl = "$SampleSourceUrl/IconsSamples.kt" + +public val IconsExamples: List = listOf( + Example( + name = "Animated Navigation bar", + description = "Show how a lbc animated nav bar could look like", + sourceUrl = IconsExampleSourceUrl, + ) { + var selected by remember { mutableStateOf(SparkAnimatedIcons.SearchToOutline.drawableId) } + var icons by remember { + mutableStateOf( + listOf( + SparkAnimatedIcons.SearchToOutline, + SparkAnimatedIcons.LikeToFill, + SparkAnimatedIcons.AddToFill, + SparkAnimatedIcons.MessageToOutline, + SparkAnimatedIcons.AccountToFillAlt, + ), + ) + } + Surface( + elevation = NavigationBarDefaults.Elevation, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp) + .windowInsetsPadding(NavigationBarDefaults.windowInsets) + .defaultMinSize(NavigationBarHeight) + .selectableGroup(), + ) { + NavigationBarItem( + selected = selected == SparkAnimatedIcons.SearchToOutline.drawableId, + onClick = { + selected = SparkAnimatedIcons.SearchToOutline.drawableId + }, + icon = { + Icon( + modifier = Modifier.size(24.dp), + sparkIcon = SparkAnimatedIcons.SearchToOutline, + contentDescription = null, + atEnd = selected != SparkAnimatedIcons.SearchToOutline.drawableId, + ) + }, + label = { + Text( + text = "Rechercher", + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + }, + ) + NavigationBarItem( + selected = selected == SparkAnimatedIcons.LikeToFill.drawableId, + onClick = { + selected = SparkAnimatedIcons.LikeToFill.drawableId + }, + icon = { + Icon( + modifier = Modifier.size(24.dp), + sparkIcon = SparkAnimatedIcons.LikeToFill, + contentDescription = null, + atEnd = selected == SparkAnimatedIcons.LikeToFill.drawableId, + ) + }, + label = { + Text( + text = "Favoris", + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + }, + ) + NavigationBarItem( + selected = selected == SparkAnimatedIcons.AddToFill.drawableId, + onClick = { + selected = SparkAnimatedIcons.AddToFill.drawableId + }, + icon = { + Icon( + modifier = Modifier.size(24.dp), + sparkIcon = SparkAnimatedIcons.AddToFill, + contentDescription = null, + atEnd = selected == SparkAnimatedIcons.AddToFill.drawableId, + ) + }, + label = { + Text( + text = "Publier", + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + }, + ) + NavigationBarItem( + selected = selected == SparkAnimatedIcons.MessageToOutline.drawableId, + onClick = { + selected = SparkAnimatedIcons.MessageToOutline.drawableId + }, + icon = { + Icon( + modifier = Modifier.size(24.dp), + sparkIcon = SparkAnimatedIcons.MessageToOutline, + contentDescription = null, + atEnd = selected != SparkAnimatedIcons.MessageToOutline.drawableId, + ) + }, + label = { + Text( + text = "Messages", + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + }, + ) + NavigationBarItem( + selected = selected == SparkAnimatedIcons.AccountToFill.drawableId, + onClick = { + selected = SparkAnimatedIcons.AccountToFill.drawableId + }, + icon = { + Icon( + modifier = Modifier.size(24.dp), + sparkIcon = SparkAnimatedIcons.AccountToFill, + contentDescription = null, + atEnd = selected == SparkAnimatedIcons.AccountToFill.drawableId, + ) + }, + label = { + Text( + text = "Compte", + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + }, + ) + } + } + }, +) + +@Composable +public fun RowScope.NavigationBarItem( + selected: Boolean, + onClick: () -> Unit, + icon: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + label: @Composable (() -> Unit)? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + val targetValue = when { + !enabled -> SparkTheme.colors.onSurface.disabled + else -> SparkTheme.colors.onSurface + } + val contentColor by animateColorAsState( + targetValue = targetValue, + animationSpec = tween(ItemAnimationDurationMillis), + ) + val styledIcon = @Composable { + + // If there's a label, don't have a11y services repeat the icon description. + val clearSemantics = label != null + Box(modifier = if (clearSemantics) Modifier.clearAndSetSemantics {} else Modifier) { + CompositionLocalProvider(LocalContentColor provides contentColor, content = icon) + } + } + + val styledLabel: @Composable (() -> Unit)? = label?.let { + @Composable { + val style = SparkTheme.typography.caption.highlight + val mergedStyle = LocalTextStyle.current.merge(style) + CompositionLocalProvider( + LocalContentColor provides contentColor, + LocalTextStyle provides mergedStyle, + content = label, + ) + } + } + + // The color of the Ripple should always the selected color, as we want to show the color + // before the item is considered selected, and hence before the new contentColor is + // provided by BottomNavigationTransition. + val ripple = rememberRipple(bounded = false, color = SparkTheme.colors.neutral) + + Box( + modifier + .selectable( + selected = selected, + onClick = onClick, + enabled = enabled, + role = Role.Tab, + interactionSource = interactionSource, + indication = ripple, + ) + .defaultMinSize(NavigationBarHeight) + .weight(1f), + contentAlignment = Alignment.Center, + propagateMinConstraints = true, + ) { + NavigationBarItemLayout( + icon = styledIcon, + label = styledLabel, + ) + } +} + +/** + * Base layout for a [NavigationBarItem]. + * + * @param icon icon for this item + * @param label text label for this item + */ +@Composable +internal fun NavigationBarItemLayout( + icon: @Composable () -> Unit, + label: @Composable (() -> Unit)?, +) { + Layout( + content = { + Box(Modifier.layoutId(IconLayoutIdTag)) { icon() } + + if (label != null) { + Box( + Modifier + .layoutId(LabelLayoutIdTag) + .padding(horizontal = NavigationBarItemHorizontalPadding / 2), + ) { label() } + } + }, + ) { measurables, constraints -> + @Suppress("NAME_SHADOWING") + val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) + val iconPlaceable = + measurables.fastFirst { it.layoutId == IconLayoutIdTag }.measure(looseConstraints) + + val labelPlaceable = + label?.let { + measurables + .fastFirst { it.layoutId == LabelLayoutIdTag } + .measure(looseConstraints) + } + + placeLabelAndIcon( + labelPlaceable!!, + iconPlaceable, + constraints, + ) + } +} + +/** + * Places the provided [Placeable]s in the correct position. + * + * @param labelPlaceable text label placeable inside this item + * @param iconPlaceable icon placeable inside this item + * @param constraints constraints of the item + */ +private fun MeasureScope.placeLabelAndIcon( + labelPlaceable: Placeable, + iconPlaceable: Placeable, + constraints: Constraints, +): MeasureResult { + val contentHeight = + iconPlaceable.height + IndicatorVerticalPadding.toPx() * 2 + labelPlaceable.height + 2.0.dp.toPx() + + // Icon (when selected) should be `contentVerticalPadding` from top + val selectedIconY = IndicatorVerticalPadding.toPx() + + // Label should be fixed padding below icon + val labelY = selectedIconY + iconPlaceable.height + 2.0.dp.toPx() + + val containerWidth = constraints.maxWidth + + val labelX = (containerWidth - labelPlaceable.width) / 2 + val iconX = (containerWidth - iconPlaceable.width) / 2 + + return layout(containerWidth, contentHeight.roundToInt()) { + labelPlaceable.placeRelative(labelX, (labelY).roundToInt()) + iconPlaceable.placeRelative(iconX, (selectedIconY).roundToInt()) + } +} + +private const val IconLayoutIdTag: String = "icon" + +private const val LabelLayoutIdTag: String = "label" + +internal val NavigationBarHeight: Dp = 58.dp + +internal const val ItemAnimationDurationMillis: Int = 100 + +/*@VisibleForTesting*/ +private val NavigationBarItemHorizontalPadding: Dp = 4.dp + +/*@VisibleForTesting*/ +private val IndicatorVerticalPadding: Dp = 8.dp diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/icons/IconDemoScreen.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/icons/IconDemoScreen.kt index 97c0ba972..842f801ea 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/icons/IconDemoScreen.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/icons/IconDemoScreen.kt @@ -50,16 +50,24 @@ public fun IconDemoScreen( ) } composable( - route = "$IconDemoRoute/{$IconIdArgName}/{$IconNameArgName}", + route = "$IconDemoRoute/{$IconIdArgName}/{$IconNameArgName}/{$IconAnimatedArgName}", arguments = listOf( navArgument(IconIdArgName) { type = NavType.IntType }, navArgument(IconNameArgName) { type = NavType.StringType }, + navArgument(IconAnimatedArgName) { type = NavType.BoolType }, ), ) { navBackStackEntry -> val arguments = requireNotNull(navBackStackEntry.arguments) { "No arguments" } + val isAnimated = arguments.getBoolean(IconAnimatedArgName) + val icon = if(isAnimated) { + SparkIcon.AnimatedDrawableRes(arguments.getInt(IconIdArgName)) + } else { + SparkIcon.DrawableRes(arguments.getInt(IconIdArgName)) + } IconExampleScreen( - icon = SparkIcon.DrawableRes(arguments.getInt(IconIdArgName)), + icon = icon, name = requireNotNull(arguments.getString(IconNameArgName)) { "No name provided for the Icon" }, + isAnimated = isAnimated, ) } }, @@ -70,3 +78,4 @@ internal const val IconsList = "icons" internal const val IconDemoRoute = "iconDemo" internal const val IconIdArgName = "iconId" internal const val IconNameArgName = "iconName" +internal const val IconAnimatedArgName = "iconAnimated" diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/icons/IconExampleScreen.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/icons/IconExampleScreen.kt index 3cfb43e6a..c307f2510 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/icons/IconExampleScreen.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/icons/IconExampleScreen.kt @@ -25,11 +25,14 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -47,9 +50,12 @@ import com.adevinta.spark.components.toggles.SwitchLabelled import com.adevinta.spark.icons.Close import com.adevinta.spark.icons.SparkIcon import com.adevinta.spark.icons.SparkIcons +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds @Composable -internal fun IconExampleScreen(icon: SparkIcon, name: String) { +internal fun IconExampleScreen(icon: SparkIcon, name: String, isAnimated: Boolean) { Column( modifier = Modifier .fillMaxSize() @@ -57,19 +63,51 @@ internal fun IconExampleScreen(icon: SparkIcon, name: String) { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically), ) { - IconButtonFilled(icon = icon, contentDescription = name, onClick = {}) - ButtonFilled(onClick = {}, text = name, icon = icon) - TagFilled(text = name, leadingIcon = icon) - ChipTinted { + var atEnd by remember { mutableStateOf(false) } + var isRunning by remember { mutableStateOf(false) } + + // This is necessary just if you want to run the animation when the + // component is displayed. Otherwise, you can remove it. + LaunchedEffect(icon, isRunning) { + while (isRunning) { + delay(3.seconds) // set here your delay between animations + atEnd = !atEnd + } + } + if (isAnimated) { + SwitchLabelled( + checked = isRunning, + onCheckedChange = { isRunning = !isRunning }, + ) { + Text(text = "Animate indefinitely") + } + } + + Icon( + sparkIcon = icon, + contentDescription = name, + modifier = Modifier.size(128.dp), + atEnd = atEnd, + ) + IconButtonFilled( + icon = icon, + contentDescription = name, + onClick = { + atEnd = !atEnd + }, + atEnd = atEnd, + ) + ButtonFilled(onClick = { atEnd = !atEnd }, text = name, icon = icon, atEnd = atEnd) + TagFilled(text = name, leadingIcon = icon, atEnd = atEnd) + ChipTinted( + onClick = { atEnd = !atEnd } + ) { Text(text = name) Icon(sparkIcon = icon, contentDescription = name) } - var checked by remember { - mutableStateOf(true) - } SwitchLabelled( - checked = checked, - onCheckedChange = { checked = it }, + checked = atEnd, + onCheckedChange = { atEnd = !atEnd }, icons = SwitchIcons(checked = icon, unchecked = SparkIcons.Close), ) { Text(text = name) diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/icons/IconsScreen.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/icons/IconsScreen.kt index d4cb92233..471a176af 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/icons/IconsScreen.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/icons/IconsScreen.kt @@ -28,10 +28,12 @@ import androidx.annotation.DrawableRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -39,8 +41,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -60,11 +65,13 @@ import androidx.navigation.NavController import com.adevinta.spark.SparkTheme import com.adevinta.spark.catalog.R import com.adevinta.spark.catalog.util.splitCamelWithSpaces +import com.adevinta.spark.components.chips.ChipSelectable +import com.adevinta.spark.components.chips.ChipStyles import com.adevinta.spark.components.icons.Icon import com.adevinta.spark.components.icons.IconSize -import com.adevinta.spark.components.spacer.VerticalSpacer import com.adevinta.spark.components.text.Text import com.adevinta.spark.components.textfields.TextField +import com.adevinta.spark.icons.Check import com.adevinta.spark.icons.DeleteFill import com.adevinta.spark.icons.Search import com.adevinta.spark.icons.SparkIcon @@ -83,27 +90,40 @@ public fun IconsScreen( ) { val context = LocalContext.current val focusManager = LocalFocusManager.current - var icons: List by remember { + var icons: List by remember { mutableStateOf(emptyList()) } LaunchedEffect(Unit) { icons = getAllIconsRes(context) } var query: String by rememberSaveable { mutableStateOf("") } - val filteredIcons by remember { + var showIcons by rememberSaveable { mutableStateOf(true) } + var showAnimatedIcons by rememberSaveable { mutableStateOf(true) } + + val filteredIcons by remember(query, showIcons, showAnimatedIcons) { derivedStateOf { - if (query.isEmpty()) icons else icons.filter { it.name.contains(query, ignoreCase = true) } + if (query.isEmpty()) { + icons + } else { + icons.filter { it.name.contains(query, ignoreCase = true) } + }.filterNot { + !showIcons && it is NamedAsset.Icon + }.filterNot { + !showAnimatedIcons && it is NamedAsset.AnimatedIcon + } } } Column( - modifier = modifier - .fillMaxSize() - .padding(16.dp), + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { TextField( value = query, onValueChange = { query = it }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 16.dp), placeholder = stringResource(id = R.string.icons_screen_search_helper), leadingContent = { Icon(sparkIcon = SparkIcons.Search, contentDescription = null) @@ -116,7 +136,29 @@ public fun IconsScreen( ) }, ) - VerticalSpacer(space = 16.dp) + FlowRow( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + ChipSelectable( + selected = showIcons, + text = "Icon", + onClick = { showIcons = !showIcons }, + style = ChipStyles.Tinted, + leadingIcon = if (showIcons) SparkIcons.Check else null + ) + ChipSelectable( + selected = showAnimatedIcons, + text = "Animated Icon", + onClick = { showAnimatedIcons = !showAnimatedIcons }, + style = ChipStyles.Tinted, + leadingIcon = if (showAnimatedIcons) SparkIcons.Check else null + ) + + } LazyVerticalGrid( modifier = modifier .consumeWindowInsets(contentPadding) @@ -128,14 +170,19 @@ public fun IconsScreen( ) { focusManager.clearFocus() }, - contentPadding = contentPadding, columns = GridCells.Adaptive(minSize = 60.dp), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), ) { - items(filteredIcons.size) { index -> - val (drawableRes, iconName) = filteredIcons[index] + items( + items = filteredIcons, + key = { it.name }, + contentType = { it is NamedAsset.Icon }, + ) { asset -> + val drawableRes = asset.drawableRes + val iconName = asset.name + val isAnimated = asset is NamedAsset.AnimatedIcon Column( modifier = Modifier .clip(SparkTheme.shapes.small) @@ -143,11 +190,12 @@ public fun IconsScreen( onLongClick = { copyToClipboard(context, iconName) }, onClick = { navController.navigate( - route = "$IconDemoRoute/$drawableRes/$iconName", + route = "$IconDemoRoute/$drawableRes/$iconName/$isAnimated", ) }, ) - .padding(8.dp), + .padding(8.dp) + .animateItemPlacement(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp), ) { @@ -167,10 +215,14 @@ public fun IconsScreen( } } -private data class NamedIcon( - @DrawableRes val drawableRes: Int, - val name: String, -) +@Stable +private sealed class NamedAsset(open val name: String, @DrawableRes open val drawableRes: Int) { + data class Icon(override val name: String, @DrawableRes override val drawableRes: Int) : + NamedAsset(name, drawableRes) + + data class AnimatedIcon(override val name: String, @DrawableRes override val drawableRes: Int) : + NamedAsset(name, drawableRes) +} private suspend fun getAllIconsRes(context: Context) = withContext(Default) { IconR.drawable::class.java.declaredFields.mapNotNull { field -> @@ -178,10 +230,14 @@ private suspend fun getAllIconsRes(context: Context) = withContext(Default) { val icon = field.getInt(null) val name = context.resources.getResourceEntryName(icon) if (!name.startsWith(prefix)) return@mapNotNull null - NamedIcon( - drawableRes = icon, - name = name.removePrefix(prefix).toPascalCase(), - ) + when { + name.contains("animated") -> { + val animatedName = name.removePrefix(prefix).removeSuffix("_animated").toPascalCase() + NamedAsset.AnimatedIcon(animatedName, icon) + } + + else -> NamedAsset.Icon(name.removePrefix(prefix).toPascalCase(), icon) + } } } diff --git a/catalog/src/main/kotlin/com/adevinta/spark/catalog/model/Components.kt b/catalog/src/main/kotlin/com/adevinta/spark/catalog/model/Components.kt index 76c5984cb..c646cfbe7 100644 --- a/catalog/src/main/kotlin/com/adevinta/spark/catalog/model/Components.kt +++ b/catalog/src/main/kotlin/com/adevinta/spark/catalog/model/Components.kt @@ -50,6 +50,7 @@ import com.adevinta.spark.catalog.examples.samples.buttons.ButtonsExamples import com.adevinta.spark.catalog.examples.samples.buttons.IconButtonsExamples import com.adevinta.spark.catalog.examples.samples.chips.ChipsExamples import com.adevinta.spark.catalog.examples.samples.dialog.DialogsExamples +import com.adevinta.spark.catalog.examples.samples.icons.IconsExamples import com.adevinta.spark.catalog.examples.samples.popover.PopoverExamples import com.adevinta.spark.catalog.examples.samples.progressbar.ProgressbarExamples import com.adevinta.spark.catalog.examples.samples.progresstracker.ProgressTrackerExamples @@ -145,6 +146,19 @@ private val Dropdowns = Component( configurator = DropdownsConfigurator, ) +private val Icons = Component( + id = nextId(), + name = "Icons", + illustration = R.drawable.illu_component_iconbutton, + tintIcon = false, + description = R.string.component_iconbutton_description, + guidelinesUrl = "$ComponentGuidelinesUrl/p/2352e9-icon-button/b/32e1a2", + docsUrl = "$PackageSummaryUrl/com.adevinta.spark.components.iconbuttons/index.html", + sourceUrl = "$SparkSourceUrl/kotlin/com/adevinta/components/iconbuttons/IconButton.kt", + examples = IconsExamples, + configurator = null, +) + private val IconButtons = Component( id = nextId(), name = "IconButtons", @@ -372,6 +386,7 @@ public val Components: List = listOf( Dialogs, Dividers, Dropdowns, + Icons, IconButtons, IconToggleButtons, Popovers, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e63132daf..229b21e7c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,7 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appCompat" } androidx-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "androidx-appCompat" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } +androidx-compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" } diff --git a/spark-icons/src/main/kotlin/com/adevinta/spark/icons/SparkAnimatedIcons.kt b/spark-icons/src/main/kotlin/com/adevinta/spark/icons/SparkAnimatedIcons.kt new file mode 100644 index 000000000..3fc0598c2 --- /dev/null +++ b/spark-icons/src/main/kotlin/com/adevinta/spark/icons/SparkAnimatedIcons.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 Adevinta + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.adevinta.spark.icons + +import com.adevinta.spark.icons.SparkIcon.AnimatedDrawableRes + +public object SparkAnimatedIcons + +public val SparkAnimatedIcons.BellShake: AnimatedDrawableRes + get() = AnimatedDrawableRes(R.drawable.spark_icons_animated_bell_shake) + +public val SparkAnimatedIcons.SearchToOutline: AnimatedDrawableRes + get() = AnimatedDrawableRes(R.drawable.spark_icons_animated_search) + +public val SparkAnimatedIcons.LikeToFill: AnimatedDrawableRes + get() = AnimatedDrawableRes(R.drawable.spark_icons_animated_like) + +public val SparkAnimatedIcons.AddToFill: AnimatedDrawableRes + get() = AnimatedDrawableRes(R.drawable.spark_icons_animated_add) + +public val SparkAnimatedIcons.MessageToOutline: AnimatedDrawableRes + get() = AnimatedDrawableRes(R.drawable.spark_icons_animated_message) + +public val SparkAnimatedIcons.AccountToFill: AnimatedDrawableRes + get() = AnimatedDrawableRes(R.drawable.spark_icons_animated_account) + +public val SparkAnimatedIcons.AccountToFillAlt: AnimatedDrawableRes + get() = AnimatedDrawableRes(R.drawable.spark_icons_animated_account_alt) diff --git a/spark-icons/src/main/kotlin/com/adevinta/spark/icons/SparkIcon.kt b/spark-icons/src/main/kotlin/com/adevinta/spark/icons/SparkIcon.kt index b28fc53a0..055942efb 100644 --- a/spark-icons/src/main/kotlin/com/adevinta/spark/icons/SparkIcon.kt +++ b/spark-icons/src/main/kotlin/com/adevinta/spark/icons/SparkIcon.kt @@ -27,5 +27,6 @@ import androidx.compose.ui.graphics.vector.ImageVector @Stable public sealed class SparkIcon { public data class DrawableRes(@androidx.annotation.DrawableRes val drawableId: Int) : SparkIcon() + public data class AnimatedDrawableRes(@androidx.annotation.DrawableRes val drawableId: Int) : SparkIcon() public data class Vector(val imageVector: ImageVector) : SparkIcon() } diff --git a/spark-icons/src/main/res/drawable/spark_icons_account_outline.xml b/spark-icons/src/main/res/drawable/spark_icons_account_outline.xml index 5a11129b6..b1d35ccb4 100644 --- a/spark-icons/src/main/res/drawable/spark_icons_account_outline.xml +++ b/spark-icons/src/main/res/drawable/spark_icons_account_outline.xml @@ -26,6 +26,11 @@ android:viewportHeight="24"> + + diff --git a/spark-icons/src/main/res/drawable/spark_icons_animated_account.xml b/spark-icons/src/main/res/drawable/spark_icons_animated_account.xml new file mode 100644 index 000000000..8edd369b3 --- /dev/null +++ b/spark-icons/src/main/res/drawable/spark_icons_animated_account.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spark-icons/src/main/res/drawable/spark_icons_animated_account_alt.xml b/spark-icons/src/main/res/drawable/spark_icons_animated_account_alt.xml new file mode 100644 index 000000000..0dadd5261 --- /dev/null +++ b/spark-icons/src/main/res/drawable/spark_icons_animated_account_alt.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spark-icons/src/main/res/drawable/spark_icons_animated_add.xml b/spark-icons/src/main/res/drawable/spark_icons_animated_add.xml new file mode 100644 index 000000000..c038152d6 --- /dev/null +++ b/spark-icons/src/main/res/drawable/spark_icons_animated_add.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spark-icons/src/main/res/drawable/spark_icons_animated_bell_shake.xml b/spark-icons/src/main/res/drawable/spark_icons_animated_bell_shake.xml new file mode 100644 index 000000000..e8da19f3d --- /dev/null +++ b/spark-icons/src/main/res/drawable/spark_icons_animated_bell_shake.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spark-icons/src/main/res/drawable/spark_icons_animated_like.xml b/spark-icons/src/main/res/drawable/spark_icons_animated_like.xml new file mode 100644 index 000000000..b7f5e50dc --- /dev/null +++ b/spark-icons/src/main/res/drawable/spark_icons_animated_like.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/spark-icons/src/main/res/drawable/spark_icons_animated_message.xml b/spark-icons/src/main/res/drawable/spark_icons_animated_message.xml new file mode 100644 index 000000000..567ff18b4 --- /dev/null +++ b/spark-icons/src/main/res/drawable/spark_icons_animated_message.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/spark-icons/src/main/res/drawable/spark_icons_animated_search.xml b/spark-icons/src/main/res/drawable/spark_icons_animated_search.xml new file mode 100644 index 000000000..41f628c92 --- /dev/null +++ b/spark-icons/src/main/res/drawable/spark_icons_animated_search.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spark-icons/src/main/res/drawable/spark_icons_message_fill.xml b/spark-icons/src/main/res/drawable/spark_icons_message_fill.xml index f908e270e..16970dd0f 100644 --- a/spark-icons/src/main/res/drawable/spark_icons_message_fill.xml +++ b/spark-icons/src/main/res/drawable/spark_icons_message_fill.xml @@ -28,4 +28,4 @@ android:fillColor="#FF000000" android:pathData="M2,6.18C2,3.84 3.9,2 6.21,2h11.5a4.28,4.28 0,0 1,4.28 4.28v14.7a1.02,1.02 0,0 1,-1.62 0.82l-3.66,-2.74H6.3a4.28,4.28 0,0 1,-4.28 -4.26c-0.01,-2.52 -0.02,-6.03 -0.01,-8.63ZM16.9,8.2H7.1a1.02,1.02 0,1 1,0 -2.04h9.8c0.56,0 1.02,0.45 1.02,1.02S17.47,8.2 16.9,8.2m0,3.19H7.1a1.02,1.02 0,1 1,0 -2.04h9.8c0.56,0 1.02,0.45 1.02,1.02s-0.45,1.02 -1.02,1.02M6.08,13.64c0,-0.56 0.45,-1.02 1.02,-1.02h6.53c0.56,0 1.02,0.45 1.02,1.02s-0.45,1.02 -1.02,1.02H7.1c-0.56,0 -1.02,-0.45 -1.02,-1.02" android:fillType="evenOdd"/> - + \ No newline at end of file diff --git a/spark-icons/src/main/res/drawable/spark_icons_message_outline.xml b/spark-icons/src/main/res/drawable/spark_icons_message_outline.xml index 526a30c51..1d5ad6990 100644 --- a/spark-icons/src/main/res/drawable/spark_icons_message_outline.xml +++ b/spark-icons/src/main/res/drawable/spark_icons_message_outline.xml @@ -25,7 +25,16 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:fillColor="#FF4F6076" + android:pathData="M16.9 8.2H7.1c-0.56 0-1.02-0.46-1.02-1.02S6.54 6.17 7.1 6.17h9.8c0.56 0 1.02 0.45 1.02 1.01S17.46 8.2 16.9 8.2Z"/> + + + diff --git a/spark/build.gradle.kts b/spark/build.gradle.kts index 5942a9634..6e2d8de26 100644 --- a/spark/build.gradle.kts +++ b/spark/build.gradle.kts @@ -66,6 +66,7 @@ dependencies { implementation(libs.androidx.savedstate) implementation(libs.androidx.window) + api(libs.androidx.compose.animation.graphics) api(libs.androidx.compose.foundation) api(libs.androidx.compose.material3) api(libs.androidx.compose.material3.windowSizeClass) diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/Button.kt b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/Button.kt index 4495e4e1b..15cbe7cbd 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/Button.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/Button.kt @@ -79,6 +79,7 @@ internal fun BaseSparkButton( isLoading: Boolean = false, contentPadding: PaddingValues = SparkButtonDefaults.buttonContentPadding(size), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { Button( @@ -113,6 +114,7 @@ internal fun BaseSparkButton( .size(SparkButtonDefaults.IconSize) .testTag("buttonIcon"), contentDescription = null, // button text should be enough for context + atEnd = atEnd, ) Spacer(Modifier.width(SparkButtonDefaults.IconSpacing)) } @@ -130,6 +132,7 @@ internal fun BaseSparkButton( .size(SparkButtonDefaults.IconSize) .testTag("buttonIcon"), contentDescription = null, // button text should be enough for context + atEnd = atEnd, ) } } @@ -151,6 +154,7 @@ internal fun SparkButton( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { BaseSparkButton( onClick = onClick, @@ -165,6 +169,7 @@ internal fun SparkButton( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) { Text(text = text) } @@ -186,6 +191,7 @@ internal fun SparkButton( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { BaseSparkButton( onClick = onClick, @@ -200,6 +206,7 @@ internal fun SparkButton( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) { Text(text = text) } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonContrast.kt b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonContrast.kt index bb40ba1fa..839f810bb 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonContrast.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonContrast.kt @@ -79,6 +79,7 @@ public fun ButtonContrast( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { val containerColor = SparkTheme.colors.surface @@ -107,6 +108,7 @@ public fun ButtonContrast( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) } @@ -145,6 +147,7 @@ public fun ButtonContrast( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { val containerColor = SparkTheme.colors.surface val colors = intent.colors() @@ -172,6 +175,7 @@ public fun ButtonContrast( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) } @@ -209,6 +213,7 @@ public fun ButtonContrast( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { val containerColor = SparkTheme.colors.surface val colors = intent.colors() @@ -235,6 +240,7 @@ public fun ButtonContrast( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonFilled.kt b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonFilled.kt index 0b75c2e54..614d5438b 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonFilled.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonFilled.kt @@ -149,6 +149,7 @@ public fun ButtonFilled( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { val backgroundColor by animateColorAsState( targetValue = intent.colors().color, @@ -177,6 +178,7 @@ public fun ButtonFilled( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) } @@ -214,6 +216,7 @@ public fun ButtonFilled( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { val backgroundColor by animateColorAsState( targetValue = intent.colors().color, @@ -239,6 +242,7 @@ public fun ButtonFilled( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonGhost.kt b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonGhost.kt index e04197214..34a599b54 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonGhost.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonGhost.kt @@ -82,6 +82,7 @@ public fun ButtonGhost( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { val contentColor by animateColorAsState( @@ -109,6 +110,7 @@ public fun ButtonGhost( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, content = content, ) } @@ -148,6 +150,7 @@ public fun ButtonGhost( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { val contentColor by animateColorAsState( targetValue = intent.colors().color, @@ -175,6 +178,7 @@ public fun ButtonGhost( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) } @@ -213,6 +217,7 @@ public fun ButtonGhost( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { val contentColor by animateColorAsState( targetValue = intent.colors().onColor, @@ -240,6 +245,7 @@ public fun ButtonGhost( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonOutlined.kt b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonOutlined.kt index 2c9542318..54c7dc679 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonOutlined.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonOutlined.kt @@ -76,6 +76,7 @@ public fun ButtonOutlined( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { val contentColor by animateColorAsState( @@ -101,6 +102,7 @@ public fun ButtonOutlined( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, content = content, ) } @@ -138,6 +140,7 @@ public fun ButtonOutlined( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { val contentColor by animateColorAsState( targetValue = intent.colors().color, @@ -163,6 +166,7 @@ public fun ButtonOutlined( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) } @@ -199,6 +203,7 @@ public fun ButtonOutlined( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { val contentColor by animateColorAsState( targetValue = intent.colors().onColor, @@ -220,6 +225,7 @@ public fun ButtonOutlined( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonTinted.kt b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonTinted.kt index db85a30f0..d5152c4a2 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonTinted.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/buttons/ButtonTinted.kt @@ -77,6 +77,7 @@ public fun ButtonTinted( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { val backgroundColor by animateColorAsState( @@ -105,6 +106,7 @@ public fun ButtonTinted( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, content = content, ) } @@ -143,6 +145,7 @@ public fun ButtonTinted( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { val backgroundColor by animateColorAsState( targetValue = intent.colors().containerColor, @@ -171,6 +174,7 @@ public fun ButtonTinted( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) } @@ -208,6 +212,7 @@ public fun ButtonTinted( iconSide: IconSide = IconSide.START, isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { val colors = ButtonDefaults.buttonColors( containerColor = intent.colors().color, @@ -226,6 +231,7 @@ public fun ButtonTinted( iconSide = iconSide, isLoading = isLoading, interactionSource = interactionSource, + atEnd = atEnd, ) } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/iconbuttons/IconButton.kt b/spark/src/main/kotlin/com/adevinta/spark/components/iconbuttons/IconButton.kt index aed2ab5aa..c27664b6d 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/iconbuttons/IconButton.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/iconbuttons/IconButton.kt @@ -92,12 +92,14 @@ internal fun SparkIconButton( border: BorderStroke? = null, contentDescription: String? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { val content: @Composable () -> Unit = { Icon( sparkIcon = icon, contentDescription = contentDescription, size = size.iconSize, + atEnd = atEnd, ) } Box(modifier = modifier) { diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/iconbuttons/IconButtonFilled.kt b/spark/src/main/kotlin/com/adevinta/spark/components/iconbuttons/IconButtonFilled.kt index 875e8182c..fef17f51d 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/iconbuttons/IconButtonFilled.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/iconbuttons/IconButtonFilled.kt @@ -71,6 +71,7 @@ public fun IconButtonFilled( size: IconButtonSize = IconButtonDefaults.DefaultSize, contentDescription: String? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + atEnd: Boolean = false, ) { val colors = IconButtonDefaults.filledIconButtonColors(intent.colors()) SparkIconButton( @@ -84,6 +85,7 @@ public fun IconButtonFilled( size = size, contentDescription = contentDescription, interactionSource = interactionSource, + atEnd = atEnd, ) } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/icons/Icons.kt b/spark/src/main/kotlin/com/adevinta/spark/components/icons/Icons.kt index ccb302526..5844a7988 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/icons/Icons.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/icons/Icons.kt @@ -22,6 +22,10 @@ package com.adevinta.spark.components.icons import androidx.appcompat.content.res.AppCompatResources.getDrawable +import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi +import androidx.compose.animation.graphics.res.animatedVectorResource +import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter +import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size @@ -71,9 +75,10 @@ public fun Icon( modifier: Modifier = Modifier, tint: Color = IconDefaults.intent.color(), size: IconSize? = null, + atEnd: Boolean = false, ) { MaterialIcon( - painter = rememberSparkIconPainter(sparkIcon), + painter = rememberSparkIconPainter(sparkIcon, atEnd), contentDescription = contentDescription, modifier = modifier .sparkUsageOverlay() @@ -186,10 +191,15 @@ public fun Icon( ) } +@OptIn(ExperimentalAnimationGraphicsApi::class) @Composable -public fun rememberSparkIconPainter(sparkIcon: SparkIcon): Painter = when (sparkIcon) { +public fun rememberSparkIconPainter(sparkIcon: SparkIcon, atEnd: Boolean = false): Painter = when (sparkIcon) { is SparkIcon.Vector -> rememberVectorPainter(sparkIcon.imageVector) is SparkIcon.DrawableRes -> rememberDrawablePainter(getDrawable(LocalContext.current, sparkIcon.drawableId)) + is SparkIcon.AnimatedDrawableRes -> { + val icon = AnimatedImageVector.animatedVectorResource(sparkIcon.drawableId) + rememberAnimatedVectorPainter(icon, atEnd) + } } @Composable diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/tags/Tag.kt b/spark/src/main/kotlin/com/adevinta/spark/components/tags/Tag.kt index 7ce6c604d..54d18f910 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/tags/Tag.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/tags/Tag.kt @@ -68,6 +68,7 @@ internal fun BaseSparkTag( border: BorderStroke? = null, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { Surface( @@ -103,6 +104,7 @@ internal fun BaseSparkTag( modifier = Modifier.size(LeadingIconSize), contentDescription = null, // The tag is associated with a mandatory label so it's okay tint = tint ?: LocalContentColor.current, + atEnd = atEnd, ) } } else { @@ -127,6 +129,7 @@ internal fun SparkTag( border: BorderStroke? = null, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { BaseSparkTag( @@ -135,6 +138,7 @@ internal fun SparkTag( border = border, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, content = content, ) } @@ -148,6 +152,7 @@ internal fun SparkTag( border: BorderStroke? = null, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, ) { require(text.isNotBlank() || leadingIcon != null) { "text can be blank only when there is an icon" @@ -158,6 +163,7 @@ internal fun SparkTag( border = border, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, ) { if (text.isNotBlank()) { Text(text = text) @@ -174,6 +180,7 @@ internal fun SparkTag( border: BorderStroke? = null, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, ) { require(text.isNotBlank() || leadingIcon != null) { "text can be blank only when there is an icon" @@ -184,6 +191,7 @@ internal fun SparkTag( border = border, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, ) { if (text.isNotBlank()) { Text(text = text) diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/tags/TagFilled.kt b/spark/src/main/kotlin/com/adevinta/spark/components/tags/TagFilled.kt index 7b8dfb6b0..a2f117757 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/tags/TagFilled.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/tags/TagFilled.kt @@ -50,6 +50,7 @@ public fun TagFilled( intent: TagIntent = TagIntent.Basic, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { SparkTag( @@ -57,6 +58,7 @@ public fun TagFilled( modifier = modifier, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, content = content, ) } @@ -76,6 +78,7 @@ public fun TagFilled( intent: TagIntent = TagIntent.Basic, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, ) { SparkTag( colors = TagDefaults.filledColors(intent), @@ -83,6 +86,7 @@ public fun TagFilled( modifier = modifier, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, ) } @@ -101,6 +105,7 @@ public fun TagFilled( intent: TagIntent = TagIntent.Basic, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, ) { SparkTag( colors = TagDefaults.filledColors(intent), @@ -108,6 +113,7 @@ public fun TagFilled( modifier = modifier, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, ) } @@ -122,6 +128,7 @@ public fun TagFilled( colors: TagColors = TagDefaults.filledColors(TagIntent.Main), leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { BaseSparkTag( @@ -129,6 +136,7 @@ public fun TagFilled( modifier = modifier, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, content = content, ) } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/tags/TagOutlined.kt b/spark/src/main/kotlin/com/adevinta/spark/components/tags/TagOutlined.kt index 23bf80900..e238a7edf 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/tags/TagOutlined.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/tags/TagOutlined.kt @@ -52,6 +52,7 @@ public fun TagOutlined( intent: TagIntent = TagIntent.Basic, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { val colors = TagDefaults.outlinedColors(intent) @@ -64,6 +65,7 @@ public fun TagOutlined( ), leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, content = content, ) } @@ -83,6 +85,7 @@ public fun TagOutlined( intent: TagIntent = TagIntent.Basic, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, ) { val colors = TagDefaults.outlinedColors(intent) SparkTag( @@ -95,6 +98,7 @@ public fun TagOutlined( ), leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd ) } @@ -113,6 +117,7 @@ public fun TagOutlined( intent: TagIntent = TagIntent.Basic, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, ) { val colors = TagDefaults.outlinedColors(intent) SparkTag( @@ -125,6 +130,7 @@ public fun TagOutlined( ), leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd ) } diff --git a/spark/src/main/kotlin/com/adevinta/spark/components/tags/TagTinted.kt b/spark/src/main/kotlin/com/adevinta/spark/components/tags/TagTinted.kt index 3d9f6b986..0d07dddb8 100644 --- a/spark/src/main/kotlin/com/adevinta/spark/components/tags/TagTinted.kt +++ b/spark/src/main/kotlin/com/adevinta/spark/components/tags/TagTinted.kt @@ -50,6 +50,7 @@ public fun TagTinted( intent: TagIntent = TagIntent.Basic, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { SparkTag( @@ -57,6 +58,7 @@ public fun TagTinted( modifier = modifier, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, content = content, ) } @@ -76,12 +78,14 @@ public fun TagTinted( intent: TagIntent = TagIntent.Basic, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, ) { SparkTag( colors = TagDefaults.tintedColors(intent), modifier = modifier, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, text = text, ) } @@ -101,12 +105,14 @@ public fun TagTinted( intent: TagIntent = TagIntent.Basic, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, ) { SparkTag( colors = TagDefaults.tintedColors(intent), modifier = modifier, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, text = text, ) } @@ -121,6 +127,7 @@ public fun TagTonal( intent: TagIntent = TagIntent.Basic, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { BaseSparkTag( @@ -128,6 +135,7 @@ public fun TagTonal( modifier = modifier, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, content = content, ) } @@ -141,6 +149,7 @@ public fun TagCriteria( modifier: Modifier = Modifier, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { TagTonal( @@ -148,6 +157,7 @@ public fun TagCriteria( intent = TagIntent.Neutral, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, content = content, ) } @@ -161,6 +171,7 @@ public fun TagService( modifier: Modifier = Modifier, leadingIcon: SparkIcon? = null, tint: Color? = null, + atEnd: Boolean = false, content: @Composable RowScope.() -> Unit, ) { TagTonal( @@ -168,6 +179,7 @@ public fun TagService( intent = TagIntent.Main, leadingIcon = leadingIcon, tint = tint, + atEnd = atEnd, content = content, ) }