From da33cc9aa4c74b4af95120bc133b5f98b0c36e4a Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Thu, 19 Mar 2026 22:38:01 -0300 Subject: [PATCH 01/19] feat(fab): add radial fab component for android --- .../button/fab/RadialFloatingActionButton.kt | 115 ++++++++++++++++++ .../fab/base/BaseFloatingActionButton.kt | 61 ++++++++++ .../button/fab/components/FabSubItem.kt | 44 +++++++ .../button/fab/model/FabMainConfig.kt | 55 +++++++++ .../components/button/fab/model/FabSubItem.kt | 25 ++++ 5 files changed, 300 insertions(+) create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/components/FabSubItem.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt new file mode 100644 index 0000000..18640e4 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt @@ -0,0 +1,115 @@ +package com.developerstring.jetco.ui.components.button.fab + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.developerstring.jetco.ui.components.button.fab.base.BaseFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem +import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem +import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.math.cos +import kotlin.math.sin + +@Composable +fun RadialFloatingActionButton( + expanded: Boolean, + items: List, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + icon: (@Composable () -> Unit)? = null, + config: FabMainConfig = FabMainConfig() +) { + // Rotate the main icon smoothly when toggling open/close + val mainIconRotation by animateFloatAsState( + targetValue = if (expanded) 45f else 0f, + animationSpec = config.animation.animationSpec, + label = "mainIconRotation" + ) + + Box( + modifier = modifier + ) { + // Sub-items — laid out behind the main FAB + items.forEachIndexed { index, item -> + val animatedAlpha by animateFloatAsState( + targetValue = if (expanded) 1f else 0f, + animationSpec = config.animation.animationSpec, + label = "alpha_$index" + ) + val startAngle = config.itemArrangement.radial.start + val endAngle = config.itemArrangement.radial.end + + val angleDeg = if (items.size == 1) { + (startAngle + endAngle) / 2.0 // single item lands at the midpoint of the arc + } else { + startAngle + (endAngle - startAngle) * (index.toDouble() / items.lastIndex) + } + val angleRad = Math.toRadians(angleDeg) + + val targetOffsetX = if (expanded) (config.itemArrangement.radius.value * cos(angleRad)).dp else 0.dp + val targetOffsetY = if (expanded) (config.itemArrangement.radius.value * sin(angleRad)).dp else 0.dp + + val offsetX = remember { Animatable(0.dp, Dp.VectorConverter) } + val offsetY = remember { Animatable(0.dp, Dp.VectorConverter) } + + val springSpec: AnimationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ) + + val tweenSpec: AnimationSpec = tween( + durationMillis = config.animation.durationMillis, + easing = config.animation.easing + ) + + LaunchedEffect(expanded) { + val staggerDelay = (index * 60).toLong() + if (expanded) { + delay(staggerDelay) + launch { offsetX.animateTo(targetOffsetX, springSpec) } + launch { offsetY.animateTo(targetOffsetY, springSpec) } + } else { + launch { offsetX.animateTo(0.dp, tweenSpec) } + launch { offsetY.animateTo(0.dp, tweenSpec) } + } + } + + SubFabItem( + item = item, + modifier = Modifier + .offset(x = offsetX.value, y = -offsetY.value) + .padding(end = (config.buttonStyle.size - item.style.size) / 2) + .graphicsLayer { alpha = animatedAlpha }, + onClick = { item.onClick() } + ) + } + + // Main FAB button + BaseFloatingActionButton( + text = null, + icon = icon, + onClick = onClick, + config = config, + modifier = Modifier.rotate(mainIconRotation) + ) + } +} + diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt new file mode 100644 index 0000000..528d4a8 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt @@ -0,0 +1,61 @@ +package com.developerstring.jetco.ui.components.button.fab.base + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig + +@Composable +internal fun BaseFloatingActionButton( + modifier: Modifier = Modifier, + text: (@Composable () -> Unit)? = null, + icon: (@Composable () -> Unit)? = null, + onClick: (() -> Unit) = {}, + config: FabMainConfig = FabMainConfig() +) { + Box( + modifier = modifier + .defaultMinSize(minWidth = config.buttonStyle.size) + .height(config.buttonStyle.size) + .clip(config.buttonStyle.shape) + .background(config.buttonStyle.color) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = LocalIndication.current + ) { onClick.invoke() }, + contentAlignment = Alignment.Center + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(config.buttonStyle.horizontalSpace), + verticalAlignment = Alignment.CenterVertically, + ) { + if (icon != null) { + icon.invoke() + } else { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = "Base FAB icon", + tint = Color.White, + modifier = Modifier.size(config.buttonStyle.size * 0.55f) + ) + } + text?.invoke() + } + } +} \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/components/FabSubItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/components/FabSubItem.kt new file mode 100644 index 0000000..9beae7b --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/components/FabSubItem.kt @@ -0,0 +1,44 @@ +package com.developerstring.jetco.ui.components.button.fab.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem + +@Composable +internal fun SubFabItem( + item: FabSubItem, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Box( + modifier = modifier + .size(item.style.size) + .clip(item.style.shape) + .background(item.style.color) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick + ), + contentAlignment = Alignment.Center + ) { + item.icon?.let { icon -> + Icon( + imageVector = icon, + contentDescription = item.title, + tint = Color.White, + modifier = Modifier.size(item.style.size * 0.55f) + ) + } + } +} \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt new file mode 100644 index 0000000..04a8e81 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt @@ -0,0 +1,55 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Stable +data class FabMainConfig( + val buttonStyle: ButtonStyle = ButtonStyle(), + val itemArrangement: ItemArrangement = ItemArrangement(), + val animation: Animation = Animation() +) { + sealed interface Orientation { + enum class Radial(val start: Double, val end: Double) : Orientation { + END(90.0, 180.0), + START(90.0, 0.0), + CENTER(0.0, 180.0) + } + + enum class Stack(val spacedBy: Dp = 40.dp) : Orientation { + TOP, + START, + END + } + } + + @Stable + open class Animation( + val durationMillis: Int = 300, + val easing: Easing = FastOutSlowInEasing, + val animationSpec: AnimationSpec = tween(durationMillis, easing = easing) + ) + + @Stable + data class ButtonStyle( + val color: Color = Color(0xFF1976D2), + val shape: Shape = CircleShape, + val horizontalSpace: Dp = 12.dp, + val size: Dp = 72.dp, + ) + + @Stable + data class ItemArrangement( + val radius: Dp = 80.dp, + val radial: Orientation.Radial = Orientation.Radial.END, + val stack: Orientation.Stack = Orientation.Stack.TOP + ) +} \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt new file mode 100644 index 0000000..1fd0cb8 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt @@ -0,0 +1,25 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Stable +data class FabSubItem( + val onClick: () -> Unit, + val title: String? = null, + val icon: ImageVector? = null, + val style: ButtonStyle = ButtonStyle(), +) { + @Stable + data class ButtonStyle( + val color: Color = Color(0xFF1976D2), + val shape: Shape = CircleShape, + val size: Dp = 52.dp + ) +} + From e60f9685bb5a191ffe1ff04faeee8648a12e108f Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Fri, 20 Mar 2026 08:56:12 -0300 Subject: [PATCH 02/19] feat(fab): add stack fab variant for android --- .../button/fab/StackFloatingActionButton.kt | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt new file mode 100644 index 0000000..0990627 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt @@ -0,0 +1,111 @@ +package com.developerstring.jetco.ui.components.button.fab + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times +import com.developerstring.jetco.ui.components.button.fab.base.BaseFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem +import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem +import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig + +@Composable +fun StackFloatingActionButton( + expanded: Boolean, + items: List, + modifier: Modifier = Modifier, + text: (@Composable () -> Unit)? = null, + icon: (@Composable () -> Unit)? = null, + onClick: () -> Unit = {}, + config: FabMainConfig = FabMainConfig() +) { + // Rotate the main icon smoothly when toggling open/close + val mainIconRotation by animateFloatAsState( + targetValue = if (expanded) 45f else 0f, + animationSpec = config.animation.animationSpec, + label = "mainIconRotation" + ) + + val stack = config.itemArrangement.stack + + // The Box anchor changes depending on which direction items spread + val alignment = when (stack) { + FabMainConfig.Orientation.Stack.TOP -> Alignment.BottomEnd + FabMainConfig.Orientation.Stack.START -> Alignment.CenterEnd + FabMainConfig.Orientation.Stack.END -> Alignment.CenterStart + } + + Box( + modifier = modifier, + contentAlignment = alignment + ) { + // Sub-items — stacked in the direction defined by Orientation.Stack + items.forEachIndexed { index, item -> + val animatedAlpha by animateFloatAsState( + targetValue = if (expanded) 1f else 0f, + animationSpec = config.animation.animationSpec, + label = "alpha_$index" + ) + + val spacing = (index + 1) * (item.style.size + stack.spacedBy) + + // Each orientation moves items along a different axis + val targetOffsetX = when (stack) { + FabMainConfig.Orientation.Stack.START -> if (expanded) -spacing else 0.dp + FabMainConfig.Orientation.Stack.END -> if (expanded) spacing else 0.dp + else -> 0.dp + } + + val targetOffsetY = when (stack) { + FabMainConfig.Orientation.Stack.TOP -> if (expanded) -spacing else 0.dp + else -> 0.dp + } + + val animatedOffsetX by animateDpAsState( + targetValue = targetOffsetX, + animationSpec = tween( + durationMillis = config.animation.durationMillis, + easing = config.animation.easing + ), + label = "offsetX_$index" + ) + + val animatedOffsetY by animateDpAsState( + targetValue = targetOffsetY, + animationSpec = tween( + durationMillis = config.animation.durationMillis, + easing = config.animation.easing + ), + label = "offsetY_$index" + ) + + SubFabItem( + item = item, + modifier = Modifier + .offset(x = animatedOffsetX, y = animatedOffsetY) + .padding(end = (config.buttonStyle.size - item.style.size) / 2) + .graphicsLayer { alpha = animatedAlpha }, + onClick = { item.onClick() } + ) + } + + // Main FAB button + BaseFloatingActionButton( + text = text, + icon = icon, + onClick = onClick, + config = config, + modifier = Modifier.rotate(mainIconRotation) + ) + } +} From 01417c74f3e81f3ad8e87da7b2e5ae9861a6de48 Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Fri, 20 Mar 2026 09:21:46 -0300 Subject: [PATCH 03/19] feat(fab): add padding to ButtonStyle for better customization --- .../ui/components/button/fab/base/BaseFloatingActionButton.kt | 2 ++ .../jetco/ui/components/button/fab/model/FabMainConfig.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt index 528d4a8..652b3aa 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add @@ -44,6 +45,7 @@ internal fun BaseFloatingActionButton( Row( horizontalArrangement = Arrangement.spacedBy(config.buttonStyle.horizontalSpace), verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(config.buttonStyle.padding) ) { if (icon != null) { icon.invoke() diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt index 04a8e81..71cf76f 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.Easing import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color @@ -44,6 +45,7 @@ data class FabMainConfig( val shape: Shape = CircleShape, val horizontalSpace: Dp = 12.dp, val size: Dp = 72.dp, + val padding: PaddingValues = PaddingValues() ) @Stable From ce74260bf96d1055c0e8883577fc16d587e415d1 Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Fri, 20 Mar 2026 10:20:02 -0300 Subject: [PATCH 04/19] fix(fab): adjust UI alignment and rotation animation --- .../button/fab/RadialFloatingActionButton.kt | 14 ++------ .../button/fab/StackFloatingActionButton.kt | 35 ++++++++++++------- .../fab/base/BaseFloatingActionButton.kt | 34 +++++++++++++----- .../button/fab/model/FabMainConfig.kt | 1 + 4 files changed, 51 insertions(+), 33 deletions(-) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt index 18640e4..ad52669 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt @@ -15,14 +15,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.developerstring.jetco.ui.components.button.fab.base.BaseFloatingActionButton import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem -import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig +import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.cos @@ -37,13 +36,6 @@ fun RadialFloatingActionButton( icon: (@Composable () -> Unit)? = null, config: FabMainConfig = FabMainConfig() ) { - // Rotate the main icon smoothly when toggling open/close - val mainIconRotation by animateFloatAsState( - targetValue = if (expanded) 45f else 0f, - animationSpec = config.animation.animationSpec, - label = "mainIconRotation" - ) - Box( modifier = modifier ) { @@ -104,11 +96,11 @@ fun RadialFloatingActionButton( // Main FAB button BaseFloatingActionButton( + expanded = expanded, text = null, icon = icon, onClick = onClick, - config = config, - modifier = Modifier.rotate(mainIconRotation) + config = config ) } } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt index 0990627..0ab5c0a 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt @@ -8,16 +8,20 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable 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.draw.rotate import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.times import com.developerstring.jetco.ui.components.button.fab.base.BaseFloatingActionButton import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem -import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig +import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem @Composable fun StackFloatingActionButton( @@ -29,14 +33,10 @@ fun StackFloatingActionButton( onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig() ) { - // Rotate the main icon smoothly when toggling open/close - val mainIconRotation by animateFloatAsState( - targetValue = if (expanded) 45f else 0f, - animationSpec = config.animation.animationSpec, - label = "mainIconRotation" - ) - val stack = config.itemArrangement.stack + val density = LocalDensity.current + var fabWidthDp by remember { mutableStateOf(config.buttonStyle.size) } + val spacedBy = stack.spacedBy // The Box anchor changes depending on which direction items spread val alignment = when (stack) { @@ -57,7 +57,10 @@ fun StackFloatingActionButton( label = "alpha_$index" ) - val spacing = (index + 1) * (item.style.size + stack.spacedBy) + val spacing = when (stack) { + FabMainConfig.Orientation.Stack.TOP -> (index + 1) * (item.style.size + spacedBy) + else -> fabWidthDp + spacedBy + index * (item.style.size + spacedBy) + } // Each orientation moves items along a different axis val targetOffsetX = when (stack) { @@ -93,19 +96,25 @@ fun StackFloatingActionButton( item = item, modifier = Modifier .offset(x = animatedOffsetX, y = animatedOffsetY) - .padding(end = (config.buttonStyle.size - item.style.size) / 2) - .graphicsLayer { alpha = animatedAlpha }, + .padding( + end = if (stack == FabMainConfig.Orientation.Stack.TOP) { + (fabWidthDp - item.style.size) / 2 + } else 0.dp + ).graphicsLayer { alpha = animatedAlpha }, onClick = { item.onClick() } ) } // Main FAB button BaseFloatingActionButton( + expanded = expanded, text = text, icon = icon, onClick = onClick, config = config, - modifier = Modifier.rotate(mainIconRotation) + modifier = Modifier.onSizeChanged { size -> + fabWidthDp = with(density) { size.width.toDp() } + } ) } } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt index 652b3aa..bb049d0 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt @@ -1,5 +1,6 @@ package com.developerstring.jetco.ui.components.button.fab.base +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -15,21 +16,31 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig @Composable internal fun BaseFloatingActionButton( + expanded: Boolean, modifier: Modifier = Modifier, text: (@Composable () -> Unit)? = null, icon: (@Composable () -> Unit)? = null, onClick: (() -> Unit) = {}, config: FabMainConfig = FabMainConfig() ) { + // Rotate the main icon smoothly when toggling open/close + val mainIconRotation by animateFloatAsState( + targetValue = if (expanded) config.buttonStyle.iconRotation else 0f, + animationSpec = config.animation.animationSpec, + label = "mainIconRotation" + ) + Box( modifier = modifier .defaultMinSize(minWidth = config.buttonStyle.size) @@ -47,15 +58,20 @@ internal fun BaseFloatingActionButton( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(config.buttonStyle.padding) ) { - if (icon != null) { - icon.invoke() - } else { - Icon( - imageVector = Icons.Rounded.Add, - contentDescription = "Base FAB icon", - tint = Color.White, - modifier = Modifier.size(config.buttonStyle.size * 0.55f) - ) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.rotate(mainIconRotation) + ) { + if (icon != null) { + icon.invoke() + } else { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = "Base FAB icon", + tint = Color.White, + modifier = Modifier.size(config.buttonStyle.size * 0.55f) + ) + } } text?.invoke() } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt index 71cf76f..05f3fbf 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt @@ -45,6 +45,7 @@ data class FabMainConfig( val shape: Shape = CircleShape, val horizontalSpace: Dp = 12.dp, val size: Dp = 72.dp, + val iconRotation: Float = 45f, val padding: PaddingValues = PaddingValues() ) From 063a4ddd2a0098752e6d3265b884478340e3ae08 Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Sat, 21 Mar 2026 13:35:06 -0300 Subject: [PATCH 05/19] feat(fab): add morph fab variant for android --- .../button/fab/MorphFloatingActionButton.kt | 134 ++++++++++++++++++ .../button/fab/RadialFloatingActionButton.kt | 2 +- .../button/fab/StackFloatingActionButton.kt | 6 +- .../button/fab/components/FabSubItem.kt | 42 ++++-- .../button/fab/model/FabMainConfig.kt | 14 +- .../components/button/fab/model/FabSubItem.kt | 14 +- 6 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt new file mode 100644 index 0000000..e2e9f5b --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt @@ -0,0 +1,134 @@ +package com.developerstring.jetco.ui.components.button.fab + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.dp +import com.developerstring.jetco.ui.components.button.fab.base.BaseFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem +import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig +import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem +import kotlinx.coroutines.delay + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun MorphFloatingActionButton( + expanded: Boolean, + items: List, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + title: (@Composable () -> Unit)? = null, + text: (@Composable () -> Unit)? = null, + icon: (@Composable () -> Unit)? = null, + config: FabMainConfig = FabMainConfig() +) { + val morph = config.itemArrangement.morph + val staggerStep = config.animation.durationMillis / (items.size + 1) + + Box( + modifier = modifier + .animateContentSize( + animationSpec = tween( + durationMillis = config.animation.durationMillis, + easing = config.animation.easing + ) + ) + .clip(if (expanded) morph.cardShape else config.buttonStyle.shape) + .background(config.buttonStyle.color) + ) { + if (expanded) { + Column( + modifier = Modifier + .width(morph.width) + .padding(16.dp) + ) { + Box(modifier = Modifier.fillMaxWidth()) { + Box(modifier = Modifier.align(Alignment.CenterStart)) { + title?.invoke() + } + Box( + modifier = Modifier + .size(32.dp) + .align(Alignment.CenterEnd) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = "Close", + tint = Color.White, + modifier = Modifier.size(20.dp) + ) + } + } + + Spacer(modifier = Modifier.size(morph.headerSpace)) + + FlowRow( + maxItemsInEachRow = morph.columns, + horizontalArrangement = Arrangement.spacedBy(morph.spacedBy, Alignment.CenterHorizontally), + verticalArrangement = Arrangement.spacedBy(morph.spacedBy), + modifier = Modifier.fillMaxWidth() + ) { + items.forEachIndexed { index, item -> + val alpha = remember { Animatable(0f) } + + LaunchedEffect(Unit) { + delay((index * staggerStep).toLong()) + alpha.animateTo( + targetValue = 1f, + animationSpec = tween( + durationMillis = (config.animation.durationMillis + ((index + 1) * 100)), + easing = FastOutSlowInEasing + ) + ) + } + + SubFabItem( + item = item, + onClick = { item.onClick() }, + modifier = Modifier.graphicsLayer { this.alpha = alpha.value } + ) + } + } + } + } else { + BaseFloatingActionButton( + expanded = false, + text = text, + icon = icon, + onClick = onClick, + config = config + ) + } + } +} diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt index ad52669..068dff5 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt @@ -88,7 +88,7 @@ fun RadialFloatingActionButton( item = item, modifier = Modifier .offset(x = offsetX.value, y = -offsetY.value) - .padding(end = (config.buttonStyle.size - item.style.size) / 2) + .padding(end = (config.buttonStyle.size - item.buttonStyle.size) / 2) .graphicsLayer { alpha = animatedAlpha }, onClick = { item.onClick() } ) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt index 0ab5c0a..b5529c1 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt @@ -58,8 +58,8 @@ fun StackFloatingActionButton( ) val spacing = when (stack) { - FabMainConfig.Orientation.Stack.TOP -> (index + 1) * (item.style.size + spacedBy) - else -> fabWidthDp + spacedBy + index * (item.style.size + spacedBy) + FabMainConfig.Orientation.Stack.TOP -> (index + 1) * (item.buttonStyle.size + spacedBy) + else -> fabWidthDp + spacedBy + index * (item.buttonStyle.size + spacedBy) } // Each orientation moves items along a different axis @@ -98,7 +98,7 @@ fun StackFloatingActionButton( .offset(x = animatedOffsetX, y = animatedOffsetY) .padding( end = if (stack == FabMainConfig.Orientation.Stack.TOP) { - (fabWidthDp - item.style.size) / 2 + (fabWidthDp - item.buttonStyle.size) / 2 } else 0.dp ).graphicsLayer { alpha = animatedAlpha }, onClick = { item.onClick() } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/components/FabSubItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/components/FabSubItem.kt index 9beae7b..cc1a6af 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/components/FabSubItem.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/components/FabSubItem.kt @@ -1,17 +1,23 @@ package com.developerstring.jetco.ui.components.button.fab.components +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem @Composable @@ -20,25 +26,41 @@ internal fun SubFabItem( modifier: Modifier = Modifier, onClick: () -> Unit ) { - Box( + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), modifier = modifier - .size(item.style.size) - .clip(item.style.shape) - .background(item.style.color) + .size(item.buttonStyle.size) + .clip(item.buttonStyle.shape) + .background(item.buttonStyle.color) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = null, + indication = LocalIndication.current, onClick = onClick - ), - contentAlignment = Alignment.Center + ) ) { item.icon?.let { icon -> Icon( imageVector = icon, contentDescription = item.title, tint = Color.White, - modifier = Modifier.size(item.style.size * 0.55f) + modifier = Modifier.size( + if (item.title != null) item.buttonStyle.size * 0.4f + else item.buttonStyle.size * 0.55f + ) + ) + } + + item.title?.let { title -> + Text( + text = title, + color = item.titleStyle.color, + fontSize = item.titleStyle.size.value.sp, + fontWeight = item.titleStyle.weight, + maxLines = item.titleStyle.maxLines, + overflow = TextOverflow.Ellipsis, + style = item.titleStyle.style ) } } -} \ No newline at end of file +} diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt index 05f3fbf..523e25e 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape @@ -30,6 +31,14 @@ data class FabMainConfig( START, END } + + data class Morph( + val columns: Int = 2, + val spacedBy: Dp = 12.dp, + val headerSpace: Dp = 20.dp, + val width: Dp = 250.dp, + val cardShape: Shape = RoundedCornerShape(24.dp) + ) : Orientation } @Stable @@ -53,6 +62,7 @@ data class FabMainConfig( data class ItemArrangement( val radius: Dp = 80.dp, val radial: Orientation.Radial = Orientation.Radial.END, - val stack: Orientation.Stack = Orientation.Stack.TOP + val stack: Orientation.Stack = Orientation.Stack.TOP, + val morph: Orientation.Morph = Orientation.Morph() ) -} \ No newline at end of file +} diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt index 1fd0cb8..cbfcea1 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -13,7 +15,8 @@ data class FabSubItem( val onClick: () -> Unit, val title: String? = null, val icon: ImageVector? = null, - val style: ButtonStyle = ButtonStyle(), + val buttonStyle: ButtonStyle = ButtonStyle(), + val titleStyle: TitleStyle = TitleStyle() ) { @Stable data class ButtonStyle( @@ -21,5 +24,14 @@ data class FabSubItem( val shape: Shape = CircleShape, val size: Dp = 52.dp ) + + @Stable + data class TitleStyle( + val color: Color = Color.White, + val size: Dp = 12.dp, + val weight: FontWeight = FontWeight.Light, + val maxLines: Int = 1, + val style: TextStyle = TextStyle.Default + ) } From 410b188d8852b789ac795eea069565c946ede97c Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Sat, 21 Mar 2026 13:48:24 -0300 Subject: [PATCH 06/19] fix(fab): add fab height tracking to fix subitem overlap in TOP orientation --- .../ui/components/button/fab/StackFloatingActionButton.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt index b5529c1..c076428 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt @@ -36,6 +36,7 @@ fun StackFloatingActionButton( val stack = config.itemArrangement.stack val density = LocalDensity.current var fabWidthDp by remember { mutableStateOf(config.buttonStyle.size) } + var fabHeightDp by remember { mutableStateOf(config.buttonStyle.size) } val spacedBy = stack.spacedBy // The Box anchor changes depending on which direction items spread @@ -58,8 +59,10 @@ fun StackFloatingActionButton( ) val spacing = when (stack) { - FabMainConfig.Orientation.Stack.TOP -> (index + 1) * (item.buttonStyle.size + spacedBy) - else -> fabWidthDp + spacedBy + index * (item.buttonStyle.size + spacedBy) + FabMainConfig.Orientation.Stack.TOP -> + fabHeightDp + spacedBy + index * (item.buttonStyle.size + spacedBy) + else -> + fabWidthDp + spacedBy + index * (item.buttonStyle.size + spacedBy) } // Each orientation moves items along a different axis @@ -114,6 +117,7 @@ fun StackFloatingActionButton( config = config, modifier = Modifier.onSizeChanged { size -> fabWidthDp = with(density) { size.width.toDp() } + fabHeightDp = with(density) { size.height.toDp() } } ) } From 60bb97dcd3150e8b53125828c05371f07bff997e Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Sat, 21 Mar 2026 14:32:50 -0300 Subject: [PATCH 07/19] docs(fab): add documentation to all FAB components and models --- .../button/fab/MorphFloatingActionButton.kt | 29 ++++++++ .../button/fab/RadialFloatingActionButton.kt | 27 +++++++- .../button/fab/StackFloatingActionButton.kt | 26 ++++++++ .../button/fab/model/FabMainConfig.kt | 66 +++++++++++++++++++ .../components/button/fab/model/FabSubItem.kt | 31 ++++++++- 5 files changed, 177 insertions(+), 2 deletions(-) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt index e2e9f5b..956f138 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt @@ -35,6 +35,34 @@ import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem import kotlinx.coroutines.delay +/** + * A Floating Action Button that morphs into an expanded card grid when activated. + * + * When collapsed, it displays a standard circular FAB. When expanded, it animates into + * a rounded card containing a title header with a close button, and a configurable grid + * of sub-action items. Sub-items fade in one by one with a staggered entrance animation. + * + * ## Example Usage: + * ```kotlin + * MorphFloatingActionButton( + * expanded = isExpanded, + * items = listOf( + * FabSubItem( + * onClick = { } + * ) + * ) + * ) + * ``` + * + * @param expanded Whether the FAB is currently expanded into card form. + * @param items List of [FabSubItem] sub-actions to display in the card grid. + * @param modifier Modifier applied to the root [Box] container. + * @param onClick Click handler for both the main FAB button and the card close button. + * @param title Optional composable rendered as the card header title. + * @param text Optional composable rendered as a text label inside the collapsed FAB button. + * @param icon Optional custom icon composable for the collapsed FAB button. + * @param config Visual and layout configuration. See [FabMainConfig]. + */ @OptIn(ExperimentalLayoutApi::class) @Composable fun MorphFloatingActionButton( @@ -67,6 +95,7 @@ fun MorphFloatingActionButton( .width(morph.width) .padding(16.dp) ) { + // Card header: title on the left, close button on the right Box(modifier = Modifier.fillMaxWidth()) { Box(modifier = Modifier.align(Alignment.CenterStart)) { title?.invoke() diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt index 068dff5..7adc7eb 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt @@ -27,6 +27,32 @@ import kotlinx.coroutines.launch import kotlin.math.cos import kotlin.math.sin +/** + * A Floating Action Button that expands sub-items radially in an arc around the main button. + * + * When expanded, sub-items fan out in a configurable arc using spring physics with a staggered + * delay, creating a natural and lively feel. When collapsed, items snap back with a clean tween. + * The arc direction and radius are fully configurable via [FabMainConfig.ItemArrangement]. + * + * ## Example Usage: + * ```kotlin + * RadialFloatingActionButton( + * expanded = isExpanded, + * items = listOf( + * FabSubItem( + * onClick = { } + * ) + * ) + * ) + * ``` + * + * @param expanded Whether the FAB is currently expanded, showing sub-items. + * @param items List of [FabSubItem] sub-actions to display when expanded. + * @param modifier Modifier applied to the root [Box] container. + * @param onClick Click handler for the main FAB button. + * @param icon Optional custom icon composable for the main button. + * @param config Visual and layout configuration. See [FabMainConfig]. + */ @Composable fun RadialFloatingActionButton( expanded: Boolean, @@ -104,4 +130,3 @@ fun RadialFloatingActionButton( ) } } - diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt index c076428..fcb6135 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt @@ -23,6 +23,32 @@ import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem +/** + * A Floating Action Button that expands sub-items linearly in a stack — above, to the left, + * or to the right of the main button. + * + * Sub-items are pushed outward using animated offsets driven by tween animations. + * + * ## Example Usage: + * ```kotlin + * StackFloatingActionButton( + * expanded = isExpanded, + * items = listOf( + * FabSubItem( + * onClick = { } + * ) + * ) + * ) + * ``` + * + * @param expanded Whether the FAB is currently expanded, showing sub-items. + * @param items List of [FabSubItem] sub-actions to display when expanded. + * @param modifier Modifier applied to the root [Box] container. + * @param text Optional composable rendered as a text label inside the main FAB button. + * @param icon Optional custom icon composable for the main button. + * @param onClick Click handler for the main FAB button. + * @param config Visual and layout configuration. See [FabMainConfig]. + */ @Composable fun StackFloatingActionButton( expanded: Boolean, diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt index 523e25e..e5a8d4e 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt @@ -13,25 +13,66 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +/** + * Main configuration class for all Floating Action Button variants in JetCo. + * + * @param buttonStyle Visual appearance of the main FAB button. See [ButtonStyle]. + * @param itemArrangement Layout and orientation of sub-items. See [ItemArrangement]. + * @param animation Animation timing and spec used across all transitions. See [Animation]. + */ @Stable data class FabMainConfig( val buttonStyle: ButtonStyle = ButtonStyle(), val itemArrangement: ItemArrangement = ItemArrangement(), val animation: Animation = Animation() ) { + + /** + * Sealed interface representing the available layout orientations for FAB sub-items. + * + * Use [Radial] for arc-based spreading, [Stack] for linear stacking, + * and [Morph] for the card expansion layout. + */ sealed interface Orientation { + + /** + * Radial orientation that spreads sub-items in an arc around the main FAB. + * + * @property start Start angle of the arc in degrees (standard math convention). + * @property end End angle of the arc in degrees. + */ enum class Radial(val start: Double, val end: Double) : Orientation { + /** Spreads items from 90° to 180° (upward and to the left). */ END(90.0, 180.0), + /** Spreads items from 90° to 0° (upward and to the right). */ START(90.0, 0.0), + /** Spreads items from 0° to 180° (full upper arc). */ CENTER(0.0, 180.0) } + /** + * Stack orientation that spreads sub-items linearly in one direction. + * + * @property spacedBy Gap between each sub-item. Default is 40.dp. + */ enum class Stack(val spacedBy: Dp = 40.dp) : Orientation { + /** Spreads items upward above the main FAB. */ TOP, + /** Spreads items to the left of the main FAB. */ START, + /** Spreads items to the right of the main FAB. */ END } + /** + * Morph orientation that expands the main FAB into a card grid. + * + * @param columns Number of sub-item columns in the card grid. Default is 2. + * @param spacedBy Gap between sub-items inside the grid. Default is 12.dp. + * @param headerSpace Space between the card header and the item grid. Default is 20.dp. + * @param width Total width of the expanded card. Default is 250.dp. + * @param cardShape Shape of the expanded card. Default is [RoundedCornerShape] with 24.dp. + */ data class Morph( val columns: Int = 2, val spacedBy: Dp = 12.dp, @@ -41,6 +82,13 @@ data class FabMainConfig( ) : Orientation } + /** + * Animation configuration shared across all FAB variants. + * + * @param durationMillis Duration of each animation in milliseconds. Default is 300. + * @param easing Easing curve applied to tween-based animations. Default is [FastOutSlowInEasing]. + * @param animationSpec Full [AnimationSpec] used for float animations such as alpha and rotation. + */ @Stable open class Animation( val durationMillis: Int = 300, @@ -48,6 +96,16 @@ data class FabMainConfig( val animationSpec: AnimationSpec = tween(durationMillis, easing = easing) ) + /** + * Visual style configuration for the main FAB button. + * + * @param color Background color of the main FAB. Default is Material blue. + * @param shape Shape of the main FAB button. Default is [CircleShape]. + * @param horizontalSpace Horizontal gap between icon and text when both are present. Default is 12.dp. + * @param size Diameter (and height) of the main FAB. Width expands via [defaultMinSize] when text is added. Default is 72.dp. + * @param iconRotation Target rotation angle of the icon when the FAB is expanded. Default is 45f. + * @param padding Internal padding applied inside the FAB button row. Default is no padding. + */ @Stable data class ButtonStyle( val color: Color = Color(0xFF1976D2), @@ -58,6 +116,14 @@ data class FabMainConfig( val padding: PaddingValues = PaddingValues() ) + /** + * Layout and orientation configuration for FAB sub-items. + * + * @param radius Distance from the main FAB center to each sub-item in radial layout. Default is 80.dp. + * @param radial Radial arc orientation. Default is [Orientation.Radial.END]. + * @param stack Stack direction orientation. Default is [Orientation.Stack.TOP]. + * @param morph Morph card configuration. Default is [Orientation.Morph]. + */ @Stable data class ItemArrangement( val radius: Dp = 80.dp, diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt index cbfcea1..5b6c3e5 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt @@ -10,6 +10,19 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +/** + * Data model representing a single sub-action item displayed by a FAB variant. + * + * Each [FabSubItem] carries an action callback, an optional icon, an optional title, + * and independent style configurations for both the button and the title label. + * The title is only rendered when the FAB variant explicitly enables it + * + * @param onClick Action invoked when the sub-item is clicked. + * @param title Optional label displayed below the icon. + * @param icon Optional icon displayed inside the sub-item button. + * @param buttonStyle Visual style of the sub-item button. See [ButtonStyle]. + * @param titleStyle Visual style of the title label. See [TitleStyle]. + */ @Stable data class FabSubItem( val onClick: () -> Unit, @@ -18,6 +31,14 @@ data class FabSubItem( val buttonStyle: ButtonStyle = ButtonStyle(), val titleStyle: TitleStyle = TitleStyle() ) { + + /** + * Visual style configuration for the sub-item button. + * + * @param color Background color of the sub-item button. Default is Material blue. + * @param shape Shape of the sub-item button. Default is [CircleShape]. + * @param size Diameter of the sub-item button. Default is 52.dp. + */ @Stable data class ButtonStyle( val color: Color = Color(0xFF1976D2), @@ -25,6 +46,15 @@ data class FabSubItem( val size: Dp = 52.dp ) + /** + * Visual style configuration for the sub-item title label. + * + * @param color Text color of the title. Default is [Color.White]. + * @param size Font size of the title in Dp. Default is 12.dp. + * @param weight Font weight of the title. Default is [FontWeight.Light]. + * @param maxLines Maximum number of lines before the text is ellipsized. Default is 1. + * @param style Base [TextStyle] applied to the title. Default is [TextStyle.Default]. + */ @Stable data class TitleStyle( val color: Color = Color.White, @@ -34,4 +64,3 @@ data class FabSubItem( val style: TextStyle = TextStyle.Default ) } - From bf427e5ddb935a86e4c7107b2c05ae004acf1d6c Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Sat, 21 Mar 2026 14:34:06 -0300 Subject: [PATCH 08/19] chore(fab): add preview composable for FAB components --- .../FloatingActionButtonPreview.kt | 170 ++++++++++++++++++ .../jetco_library/MainActivity.kt | 11 +- 2 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt diff --git a/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt b/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt new file mode 100644 index 0000000..b5bbb17 --- /dev/null +++ b/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt @@ -0,0 +1,170 @@ +package com.developerstring.jetco_library + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.MailOutline +import androidx.compose.material.icons.outlined.Place +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.developerstring.jetco.ui.components.button.fab.MorphFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.RadialFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.StackFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig +import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem + +private val items = listOf( + FabSubItem( + onClick = { println("handle home") }, + icon = Icons.Outlined.Home, + buttonStyle = FabSubItem.ButtonStyle( + color = Color(0xFFE46212), + size = 64.dp + ) + ), + FabSubItem( + onClick = { println("handle mail") }, + icon = Icons.Outlined.MailOutline, + buttonStyle = FabSubItem.ButtonStyle( + color = Color(0xFF3DBFE2), + size = 64.dp + ) + ), + FabSubItem( + onClick = { println("handle place") }, + icon = Icons.Outlined.Place, + buttonStyle = FabSubItem.ButtonStyle( + color = Color(0xFF4AA651), + size = 64.dp + ) + ), + FabSubItem( + onClick = { println("handle delete") }, + icon = Icons.Outlined.Delete, + buttonStyle = FabSubItem.ButtonStyle( + color = Color(0xFFDE3B3D), + size = 64.dp + ) + ) +) + +@Composable +fun RadialFloatingActionButtonPreview() { + var expanded by remember { mutableStateOf(false) } + + RadialFloatingActionButton( + expanded = expanded, + items = items, + onClick = { expanded = !expanded }, + config = FabMainConfig( + itemArrangement = FabMainConfig.ItemArrangement( + radius = 144.dp + ), + buttonStyle = FabMainConfig.ButtonStyle( + color = Color(0xFFE46212), + size = 84.dp + ) + ) + ) +} + +@Composable +fun StackFloatingActionButtonPreview() { + var expanded by remember { mutableStateOf(false) } + + StackFloatingActionButton( + expanded = expanded, + items = items, + onClick = { expanded = !expanded }, + config = FabMainConfig( + buttonStyle = FabMainConfig.ButtonStyle( + color = Color(0xFFE46212), + size = 84.dp + ) + ) + ) +} + +@Composable +fun MorphFloatingActionButtonPreview() { + var expanded by remember { mutableStateOf(false) } + + MorphFloatingActionButton( + expanded = expanded, + items = listOf( + FabSubItem( + onClick = { println("handle home") }, + title = "Home", + icon = Icons.Outlined.Home, + buttonStyle = FabSubItem.ButtonStyle( + color = Color(0xFFE7722A), + shape = RoundedCornerShape(12.dp), + size = 100.dp + ), + titleStyle = FabSubItem.TitleStyle( + weight = FontWeight.Light + ) + ), + FabSubItem( + onClick = { println("handle mail") }, + title = "Mail", + icon = Icons.Outlined.MailOutline, + buttonStyle = FabSubItem.ButtonStyle( + color = Color(0xFFE7722A), + shape = RoundedCornerShape(12.dp), + size = 100.dp + ), + titleStyle = FabSubItem.TitleStyle( + weight = FontWeight.Light + ) + ), + FabSubItem( + onClick = { println("handle place") }, + title = "Place", + icon = Icons.Outlined.Place, + buttonStyle = FabSubItem.ButtonStyle( + color = Color(0xFFE7722A), + shape = RoundedCornerShape(12.dp), + size = 100.dp + ), + titleStyle = FabSubItem.TitleStyle( + weight = FontWeight.Light + ) + ), + FabSubItem( + onClick = { println("handle delete") }, + title = "Delete", + icon = Icons.Outlined.Delete, + buttonStyle = FabSubItem.ButtonStyle( + color = Color(0xFFE7722A), + shape = RoundedCornerShape(12.dp), + size = 100.dp + ), + titleStyle = FabSubItem.TitleStyle( + weight = FontWeight.Light + ) + ) + ), + onClick = { expanded = !expanded }, + title = { + Text( + text = "Quick Actions", + color = Color.White + ) + }, + config = FabMainConfig( + buttonStyle = FabMainConfig.ButtonStyle( + color = Color(0xFFE46212), + size = 84.dp + ) + ) + ) +} \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/MainActivity.kt b/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/MainActivity.kt index b9194c3..824c975 100644 --- a/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/MainActivity.kt +++ b/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/MainActivity.kt @@ -1,9 +1,12 @@ package com.developerstring.jetco_library +import android.annotation.SuppressLint import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold import androidx.compose.ui.graphics.Color import com.developerstring.jetco_library.ui.theme.JetCoLibraryTheme @@ -12,12 +15,18 @@ val LightBlue = Color(0xFFB5DAFF) val LightestPink = Color(0xFFF7F1FF) class MainActivity : ComponentActivity() { + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { JetCoLibraryTheme { - LineGraphScreen() + Scaffold( + floatingActionButton = { + MorphFloatingActionButtonPreview() + }, + floatingActionButtonPosition = FabPosition.End + ) {} } } } From e8b7786f92eb9c5882d1b1d255edce03b3d3c996 Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Wed, 25 Mar 2026 13:56:02 -0300 Subject: [PATCH 09/19] chore: rename BaseFloatingActionButton to DefaultFloatingActionButton --- ...tton.kt => DefaultFloatingActionButton.kt} | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) rename jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/{BaseFloatingActionButton.kt => DefaultFloatingActionButton.kt} (82%) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFloatingActionButton.kt similarity index 82% rename from jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt rename to jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFloatingActionButton.kt index bb049d0..946b853 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/BaseFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFloatingActionButton.kt @@ -26,11 +26,9 @@ import androidx.compose.ui.graphics.Color import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig @Composable -internal fun BaseFloatingActionButton( +internal fun DefaultFloatingActionButton( expanded: Boolean, modifier: Modifier = Modifier, - text: (@Composable () -> Unit)? = null, - icon: (@Composable () -> Unit)? = null, onClick: (() -> Unit) = {}, config: FabMainConfig = FabMainConfig() ) { @@ -62,18 +60,13 @@ internal fun BaseFloatingActionButton( contentAlignment = Alignment.Center, modifier = Modifier.rotate(mainIconRotation) ) { - if (icon != null) { - icon.invoke() - } else { - Icon( - imageVector = Icons.Rounded.Add, - contentDescription = "Base FAB icon", - tint = Color.White, - modifier = Modifier.size(config.buttonStyle.size * 0.55f) - ) - } + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = "Base FAB icon", + tint = Color.White, + modifier = Modifier.size(config.buttonStyle.size * 0.55f) + ) } - text?.invoke() } } } \ No newline at end of file From bb343266134996c107d71d3e5f3b271ae8e6510f Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Wed, 25 Mar 2026 14:00:42 -0300 Subject: [PATCH 10/19] feat(fab): add content slot for custom UI --- .../button/fab/MorphFloatingActionButton.kt | 25 ++++++++--------- .../button/fab/RadialFloatingActionButton.kt | 23 +++++++-------- .../button/fab/StackFloatingActionButton.kt | 28 +++++++++---------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt index 956f138..aa40b2d 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp -import com.developerstring.jetco.ui.components.button.fab.base.BaseFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingActionButton import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem @@ -59,8 +59,6 @@ import kotlinx.coroutines.delay * @param modifier Modifier applied to the root [Box] container. * @param onClick Click handler for both the main FAB button and the card close button. * @param title Optional composable rendered as the card header title. - * @param text Optional composable rendered as a text label inside the collapsed FAB button. - * @param icon Optional custom icon composable for the collapsed FAB button. * @param config Visual and layout configuration. See [FabMainConfig]. */ @OptIn(ExperimentalLayoutApi::class) @@ -71,9 +69,14 @@ fun MorphFloatingActionButton( modifier: Modifier = Modifier, onClick: () -> Unit = {}, title: (@Composable () -> Unit)? = null, - text: (@Composable () -> Unit)? = null, - icon: (@Composable () -> Unit)? = null, - config: FabMainConfig = FabMainConfig() + config: FabMainConfig = FabMainConfig(), + content: (@Composable () -> Unit) = { + DefaultFloatingActionButton( + expanded = false, + onClick = onClick, + config = config + ) + } ) { val morph = config.itemArrangement.morph val staggerStep = config.animation.durationMillis / (items.size + 1) @@ -151,13 +154,9 @@ fun MorphFloatingActionButton( } } } else { - BaseFloatingActionButton( - expanded = false, - text = text, - icon = icon, - onClick = onClick, - config = config - ) + Box(contentAlignment = Alignment.Center) { + content() + } } } } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt index 7adc7eb..98fbdca 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.developerstring.jetco.ui.components.button.fab.base.BaseFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingActionButton import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem @@ -50,7 +50,6 @@ import kotlin.math.sin * @param items List of [FabSubItem] sub-actions to display when expanded. * @param modifier Modifier applied to the root [Box] container. * @param onClick Click handler for the main FAB button. - * @param icon Optional custom icon composable for the main button. * @param config Visual and layout configuration. See [FabMainConfig]. */ @Composable @@ -59,8 +58,14 @@ fun RadialFloatingActionButton( items: List, modifier: Modifier = Modifier, onClick: () -> Unit = {}, - icon: (@Composable () -> Unit)? = null, - config: FabMainConfig = FabMainConfig() + config: FabMainConfig = FabMainConfig(), + content: (@Composable () -> Unit) = { + DefaultFloatingActionButton( + expanded = expanded, + onClick = onClick, + config = config + ) + } ) { Box( modifier = modifier @@ -121,12 +126,8 @@ fun RadialFloatingActionButton( } // Main FAB button - BaseFloatingActionButton( - expanded = expanded, - text = null, - icon = icon, - onClick = onClick, - config = config - ) + Box { + content() + } } } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt index fcb6135..7d589cb 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.times -import com.developerstring.jetco.ui.components.button.fab.base.BaseFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingActionButton import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem @@ -44,8 +44,6 @@ import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem * @param expanded Whether the FAB is currently expanded, showing sub-items. * @param items List of [FabSubItem] sub-actions to display when expanded. * @param modifier Modifier applied to the root [Box] container. - * @param text Optional composable rendered as a text label inside the main FAB button. - * @param icon Optional custom icon composable for the main button. * @param onClick Click handler for the main FAB button. * @param config Visual and layout configuration. See [FabMainConfig]. */ @@ -54,10 +52,15 @@ fun StackFloatingActionButton( expanded: Boolean, items: List, modifier: Modifier = Modifier, - text: (@Composable () -> Unit)? = null, - icon: (@Composable () -> Unit)? = null, onClick: () -> Unit = {}, - config: FabMainConfig = FabMainConfig() + config: FabMainConfig = FabMainConfig(), + content: (@Composable () -> Unit) = { + DefaultFloatingActionButton( + expanded = expanded, + onClick = onClick, + config = config + ) + } ) { val stack = config.itemArrangement.stack val density = LocalDensity.current @@ -134,17 +137,14 @@ fun StackFloatingActionButton( ) } - // Main FAB button - BaseFloatingActionButton( - expanded = expanded, - text = text, - icon = icon, - onClick = onClick, - config = config, + // Main FAB + Box( modifier = Modifier.onSizeChanged { size -> fabWidthDp = with(density) { size.width.toDp() } fabHeightDp = with(density) { size.height.toDp() } } - ) + ) { + content() + } } } From 660e729b66b8d2ad76cda4dbfe1569ef72332062 Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Thu, 26 Mar 2026 10:12:06 -0300 Subject: [PATCH 11/19] feat(fab): refine sub item animation behavior --- .../button/fab/MorphFloatingActionButton.kt | 37 ++++++---- .../button/fab/RadialFloatingActionButton.kt | 59 +++++++--------- .../button/fab/StackFloatingActionButton.kt | 70 +++++++++++-------- .../button/fab/model/FabMainConfig.kt | 53 ++++++++------ .../button/fab/model/FabTransition.kt | 53 ++++++++++++++ 5 files changed, 177 insertions(+), 95 deletions(-) create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabTransition.kt diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt index aa40b2d..b97f42b 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt @@ -25,7 +25,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp @@ -34,6 +33,7 @@ import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * A Floating Action Button that morphs into an expanded card grid when activated. @@ -79,18 +79,19 @@ fun MorphFloatingActionButton( } ) { val morph = config.itemArrangement.morph - val staggerStep = config.animation.durationMillis / (items.size + 1) Box( modifier = modifier .animateContentSize( animationSpec = tween( - durationMillis = config.animation.durationMillis, - easing = config.animation.easing + durationMillis = 300, + easing = FastOutSlowInEasing ) ) - .clip(if (expanded) morph.cardShape else config.buttonStyle.shape) - .background(config.buttonStyle.color) + .background( + color = if (expanded) config.buttonStyle.color else Color.Transparent, + shape = if (expanded) morph.cardShape else config.buttonStyle.shape + ) ) { if (expanded) { Column( @@ -135,14 +136,24 @@ fun MorphFloatingActionButton( val alpha = remember { Animatable(0f) } LaunchedEffect(Unit) { - delay((index * staggerStep).toLong()) - alpha.animateTo( - targetValue = 1f, - animationSpec = tween( - durationMillis = (config.animation.durationMillis + ((index + 1) * 100)), - easing = FastOutSlowInEasing - ) + val stepMs = 300 / (items.size + 1) + val order = if (expanded) config.animation.enterOrder else config.animation.exitOrder + val staggerDelay = order.delayFor( + index = index, + total = items.size, + stepMs = stepMs ) + val transition = if (expanded) config.animation.enterTransition else config.animation.exitTransition + + delay(staggerDelay) + + val targetAlpha = if (expanded) 1f else 0f + + if (transition.alphaSpec != null) { + launch { alpha.animateTo(targetAlpha, transition.alphaSpec) } + } else { + alpha.snapTo(targetAlpha) + } } SubFabItem( diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt index 98fbdca..541bd1c 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt @@ -1,18 +1,12 @@ package com.developerstring.jetco.ui.components.button.fab import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.Spring import androidx.compose.animation.core.VectorConverter -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer @@ -72,13 +66,8 @@ fun RadialFloatingActionButton( ) { // Sub-items — laid out behind the main FAB items.forEachIndexed { index, item -> - val animatedAlpha by animateFloatAsState( - targetValue = if (expanded) 1f else 0f, - animationSpec = config.animation.animationSpec, - label = "alpha_$index" - ) - val startAngle = config.itemArrangement.radial.start - val endAngle = config.itemArrangement.radial.end + val startAngle = config.itemArrangement.radial.arc.start + val endAngle = config.itemArrangement.radial.arc.end val angleDeg = if (items.size == 1) { (startAngle + endAngle) / 2.0 // single item lands at the midpoint of the arc @@ -87,31 +76,37 @@ fun RadialFloatingActionButton( } val angleRad = Math.toRadians(angleDeg) - val targetOffsetX = if (expanded) (config.itemArrangement.radius.value * cos(angleRad)).dp else 0.dp - val targetOffsetY = if (expanded) (config.itemArrangement.radius.value * sin(angleRad)).dp else 0.dp + val targetOffsetX = if (expanded) (config.itemArrangement.radial.radius.value * cos(angleRad)).dp else 0.dp + val targetOffsetY = if (expanded) (config.itemArrangement.radial.radius.value * sin(angleRad)).dp else 0.dp + val alpha = remember { Animatable(0f) } val offsetX = remember { Animatable(0.dp, Dp.VectorConverter) } val offsetY = remember { Animatable(0.dp, Dp.VectorConverter) } - val springSpec: AnimationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessMedium - ) + LaunchedEffect(expanded) { + val stepMs = 300 / (items.size + 1) + val order = if (expanded) config.animation.enterOrder else config.animation.exitOrder + val transition = if (expanded) config.animation.enterTransition else config.animation.exitTransition + val staggerDelay = order.delayFor(index = index, total = items.size, stepMs = stepMs) - val tweenSpec: AnimationSpec = tween( - durationMillis = config.animation.durationMillis, - easing = config.animation.easing - ) + delay(staggerDelay) - LaunchedEffect(expanded) { - val staggerDelay = (index * 60).toLong() - if (expanded) { - delay(staggerDelay) - launch { offsetX.animateTo(targetOffsetX, springSpec) } - launch { offsetY.animateTo(targetOffsetY, springSpec) } + val targetX = if (expanded) targetOffsetX else targetOffsetX + val targetY = if (expanded) targetOffsetY else targetOffsetY + val targetAlpha = if (expanded) 1f else 0f + + if (transition.offsetSpec != null) { + launch { offsetX.animateTo(targetX, transition.offsetSpec) } + launch { offsetY.animateTo(targetY, transition.offsetSpec) } + } else if (expanded) { + offsetX.snapTo(targetX) + offsetY.snapTo(targetY) + } + + if (transition.alphaSpec != null) { + launch { alpha.animateTo(targetAlpha, transition.alphaSpec) } } else { - launch { offsetX.animateTo(0.dp, tweenSpec) } - launch { offsetY.animateTo(0.dp, tweenSpec) } + alpha.snapTo(targetAlpha) } } @@ -120,7 +115,7 @@ fun RadialFloatingActionButton( modifier = Modifier .offset(x = offsetX.value, y = -offsetY.value) .padding(end = (config.buttonStyle.size - item.buttonStyle.size) / 2) - .graphicsLayer { alpha = animatedAlpha }, + .graphicsLayer { this.alpha = alpha.value }, onClick = { item.onClick() } ) } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt index 7d589cb..b226e27 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt @@ -1,12 +1,12 @@ package com.developerstring.jetco.ui.components.button.fab -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding 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 @@ -16,12 +16,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.times import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingActionButton import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * A Floating Action Button that expands sub-items linearly in a stack — above, to the left, @@ -81,11 +84,7 @@ fun StackFloatingActionButton( ) { // Sub-items — stacked in the direction defined by Orientation.Stack items.forEachIndexed { index, item -> - val animatedAlpha by animateFloatAsState( - targetValue = if (expanded) 1f else 0f, - animationSpec = config.animation.animationSpec, - label = "alpha_$index" - ) + val spacing = when (stack) { FabMainConfig.Orientation.Stack.TOP -> @@ -96,43 +95,56 @@ fun StackFloatingActionButton( // Each orientation moves items along a different axis val targetOffsetX = when (stack) { - FabMainConfig.Orientation.Stack.START -> if (expanded) -spacing else 0.dp - FabMainConfig.Orientation.Stack.END -> if (expanded) spacing else 0.dp + FabMainConfig.Orientation.Stack.START -> -spacing + FabMainConfig.Orientation.Stack.END -> spacing else -> 0.dp } val targetOffsetY = when (stack) { - FabMainConfig.Orientation.Stack.TOP -> if (expanded) -spacing else 0.dp + FabMainConfig.Orientation.Stack.TOP -> -spacing else -> 0.dp } - val animatedOffsetX by animateDpAsState( - targetValue = targetOffsetX, - animationSpec = tween( - durationMillis = config.animation.durationMillis, - easing = config.animation.easing - ), - label = "offsetX_$index" - ) + val alpha = remember { Animatable(0f) } + val offsetX = remember { Animatable(0.dp, Dp.VectorConverter) } + val offsetY = remember { Animatable(0.dp, Dp.VectorConverter) } - val animatedOffsetY by animateDpAsState( - targetValue = targetOffsetY, - animationSpec = tween( - durationMillis = config.animation.durationMillis, - easing = config.animation.easing - ), - label = "offsetY_$index" - ) + LaunchedEffect(expanded) { + val stepMs = 300 / (items.size + 1) + val order = if (expanded) config.animation.enterOrder else config.animation.exitOrder + val transition = if (expanded) config.animation.enterTransition else config.animation.exitTransition + val staggerDelay = order.delayFor(index = index, total = items.size, stepMs = stepMs) + + delay(staggerDelay) + + val targetX = if (expanded) targetOffsetX else 0.dp + val targetY = if (expanded) targetOffsetY else 0.dp + val targetAlpha = if (expanded) 1f else 0f + + if (transition.offsetSpec != null) { + launch { offsetX.animateTo(targetX, transition.offsetSpec) } + launch { offsetY.animateTo(targetY, transition.offsetSpec) } + } else if (expanded) { + offsetX.snapTo(targetX) + offsetY.snapTo(targetY) + } + + if (transition.alphaSpec != null) { + launch { alpha.animateTo(targetAlpha, transition.alphaSpec) } + } else { + alpha.snapTo(targetAlpha) + } + } SubFabItem( item = item, modifier = Modifier - .offset(x = animatedOffsetX, y = animatedOffsetY) + .offset(x = offsetX.value, y = offsetY.value) .padding( end = if (stack == FabMainConfig.Orientation.Stack.TOP) { (fabWidthDp - item.buttonStyle.size) / 2 } else 0.dp - ).graphicsLayer { alpha = animatedAlpha }, + ).graphicsLayer { this.alpha = alpha.value }, onClick = { item.onClick() } ) } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt index e5a8d4e..9e388c7 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt @@ -3,6 +3,8 @@ package com.developerstring.jetco.ui.components.button.fab.model import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.Easing import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.CircleShape @@ -34,20 +36,18 @@ data class FabMainConfig( * and [Morph] for the card expansion layout. */ sealed interface Orientation { - - /** - * Radial orientation that spreads sub-items in an arc around the main FAB. - * - * @property start Start angle of the arc in degrees (standard math convention). - * @property end End angle of the arc in degrees. - */ - enum class Radial(val start: Double, val end: Double) : Orientation { - /** Spreads items from 90° to 180° (upward and to the left). */ - END(90.0, 180.0), - /** Spreads items from 90° to 0° (upward and to the right). */ - START(90.0, 0.0), - /** Spreads items from 0° to 180° (full upper arc). */ - CENTER(0.0, 180.0) + data class Radial( + val arc: Arc = Arc.END, + val radius: Dp = 80.dp + ) : Orientation { + enum class Arc(val start: Double, val end: Double) : Orientation { + /** Spreads items from 90° to 180° (upward and to the left). */ + END(90.0, 180.0), + /** Spreads items from 90° to 0° (upward and to the right). */ + START(90.0, 0.0), + /** Spreads items from 0° to 180° (full upper arc). */ + CENTER(0.0, 180.0) + } } /** @@ -91,11 +91,25 @@ data class FabMainConfig( */ @Stable open class Animation( - val durationMillis: Int = 300, - val easing: Easing = FastOutSlowInEasing, - val animationSpec: AnimationSpec = tween(durationMillis, easing = easing) + val enterOrder: StaggerOrder = StaggerOrder.FIFO, + val exitOrder: StaggerOrder = StaggerOrder.FILO, + val enterTransition: FabTransition = FabTransition.Spring() + FabTransition.Fade(), + val exitTransition: FabTransition = FabTransition.Slide() + FabTransition.Fade() ) + @Stable + enum class StaggerOrder { + FIFO, + FILO, + ALL; + + internal fun delayFor(index: Int, total: Int, stepMs: Int): Long = when (this) { + FIFO -> (index * stepMs).toLong() + FILO -> ((total - 1 - index) * stepMs).toLong() + ALL -> 0L + } + } + /** * Visual style configuration for the main FAB button. * @@ -119,15 +133,12 @@ data class FabMainConfig( /** * Layout and orientation configuration for FAB sub-items. * - * @param radius Distance from the main FAB center to each sub-item in radial layout. Default is 80.dp. - * @param radial Radial arc orientation. Default is [Orientation.Radial.END]. * @param stack Stack direction orientation. Default is [Orientation.Stack.TOP]. * @param morph Morph card configuration. Default is [Orientation.Morph]. */ @Stable data class ItemArrangement( - val radius: Dp = 80.dp, - val radial: Orientation.Radial = Orientation.Radial.END, + val radial: Orientation.Radial = Orientation.Radial(), val stack: Orientation.Stack = Orientation.Stack.TOP, val morph: Orientation.Morph = Orientation.Morph() ) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabTransition.kt new file mode 100644 index 0000000..8d11a55 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabTransition.kt @@ -0,0 +1,53 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.ui.unit.Dp + +class FabTransition internal constructor( + internal val offsetSpec: AnimationSpec?, + internal val alphaSpec: AnimationSpec? +) { + operator fun plus(other: FabTransition): FabTransition = FabTransition( + offsetSpec = other.offsetSpec ?: this.offsetSpec, + alphaSpec = other.alphaSpec ?: this.alphaSpec + ) + + companion object { + fun Spring( + dampingRatio: Float = Spring.DampingRatioMediumBouncy, + stiffness: Float = Spring.StiffnessMedium + ): FabTransition = FabTransition( + offsetSpec = spring(dampingRatio, stiffness), + alphaSpec = null + ) + + fun Slide( + durationMillis: Int = 300, + easing: Easing = FastOutSlowInEasing + ): FabTransition = FabTransition( + offsetSpec = tween(durationMillis, easing = easing), + alphaSpec = null + ) + + fun Fade( + durationMillis: Int = 300, + easing: Easing = FastOutSlowInEasing + ): FabTransition = FabTransition( + offsetSpec = null, + alphaSpec = tween(durationMillis, easing = easing) + ) + + fun SlideAndFade( + durationMillis: Int = 300, + easing: Easing = FastOutSlowInEasing + ): FabTransition = FabTransition( + offsetSpec = tween(durationMillis, easing = easing), + alphaSpec = tween(durationMillis, easing = easing) + ) + } +} \ No newline at end of file From 19b326439dfe496ab38cf73f4e17d1005162a585 Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Fri, 27 Mar 2026 08:19:08 -0300 Subject: [PATCH 12/19] feat(fab): allow custom morph card implementation --- .../button/fab/MorphFloatingActionButton.kt | 141 ++++++------------ .../button/fab/base/DefaultMorphCard.kt | 72 +++++++++ .../button/fab/scope/MorphCardScope.kt | 10 ++ 3 files changed, 131 insertions(+), 92 deletions(-) create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultMorphCard.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/scope/MorphCardScope.kt diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt index b97f42b..a69cd26 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt @@ -4,34 +4,25 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.unit.dp import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.base.DefaultMorphCard import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem +import com.developerstring.jetco.ui.components.button.fab.scope.MorphCardScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -58,7 +49,6 @@ import kotlinx.coroutines.launch * @param items List of [FabSubItem] sub-actions to display in the card grid. * @param modifier Modifier applied to the root [Box] container. * @param onClick Click handler for both the main FAB button and the card close button. - * @param title Optional composable rendered as the card header title. * @param config Visual and layout configuration. See [FabMainConfig]. */ @OptIn(ExperimentalLayoutApi::class) @@ -68,17 +58,57 @@ fun MorphFloatingActionButton( items: List, modifier: Modifier = Modifier, onClick: () -> Unit = {}, - title: (@Composable () -> Unit)? = null, config: FabMainConfig = FabMainConfig(), content: (@Composable () -> Unit) = { - DefaultFloatingActionButton( - expanded = false, - onClick = onClick, - config = config - ) + DefaultFloatingActionButton(expanded = false, onClick = onClick, config = config) + }, + card: @Composable MorphCardScope.() -> Unit = { + DefaultMorphCard(config = config, onClose = onClick, scope = this) } ) { val morph = config.itemArrangement.morph + val itemsContent: @Composable () -> Unit = { + Spacer(modifier = Modifier.size(morph.headerSpace)) + + FlowRow( + maxItemsInEachRow = morph.columns, + horizontalArrangement = Arrangement.spacedBy(morph.spacedBy, Alignment.CenterHorizontally), + verticalArrangement = Arrangement.spacedBy(morph.spacedBy), + modifier = Modifier.fillMaxWidth() + ) { + items.forEachIndexed { index, item -> + // key = index ensures remember is stable per item slot + val alpha = remember(index) { Animatable(0f) } + + LaunchedEffect(index) { + val transition = config.animation.enterTransition + val stepMs = 300 / (items.size + 1) + val staggerDelay = config.animation.enterOrder.delayFor( + index = index, + total = items.size, + stepMs = stepMs + ) + + delay(staggerDelay) + + if (transition.alphaSpec != null) { + launch { alpha.animateTo(1f, transition.alphaSpec) } + } else { + alpha.snapTo(1f) + } + } + + SubFabItem( + item = item, + onClick = { item.onClick() }, + modifier = Modifier.graphicsLayer { this.alpha = alpha.value } + ) + } + } + } + val scope = MorphCardScope( + itemsContent = itemsContent + ) Box( modifier = modifier @@ -88,82 +118,9 @@ fun MorphFloatingActionButton( easing = FastOutSlowInEasing ) ) - .background( - color = if (expanded) config.buttonStyle.color else Color.Transparent, - shape = if (expanded) morph.cardShape else config.buttonStyle.shape - ) ) { if (expanded) { - Column( - modifier = Modifier - .width(morph.width) - .padding(16.dp) - ) { - // Card header: title on the left, close button on the right - Box(modifier = Modifier.fillMaxWidth()) { - Box(modifier = Modifier.align(Alignment.CenterStart)) { - title?.invoke() - } - Box( - modifier = Modifier - .size(32.dp) - .align(Alignment.CenterEnd) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = onClick - ), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Rounded.Close, - contentDescription = "Close", - tint = Color.White, - modifier = Modifier.size(20.dp) - ) - } - } - - Spacer(modifier = Modifier.size(morph.headerSpace)) - - FlowRow( - maxItemsInEachRow = morph.columns, - horizontalArrangement = Arrangement.spacedBy(morph.spacedBy, Alignment.CenterHorizontally), - verticalArrangement = Arrangement.spacedBy(morph.spacedBy), - modifier = Modifier.fillMaxWidth() - ) { - items.forEachIndexed { index, item -> - val alpha = remember { Animatable(0f) } - - LaunchedEffect(Unit) { - val stepMs = 300 / (items.size + 1) - val order = if (expanded) config.animation.enterOrder else config.animation.exitOrder - val staggerDelay = order.delayFor( - index = index, - total = items.size, - stepMs = stepMs - ) - val transition = if (expanded) config.animation.enterTransition else config.animation.exitTransition - - delay(staggerDelay) - - val targetAlpha = if (expanded) 1f else 0f - - if (transition.alphaSpec != null) { - launch { alpha.animateTo(targetAlpha, transition.alphaSpec) } - } else { - alpha.snapTo(targetAlpha) - } - } - - SubFabItem( - item = item, - onClick = { item.onClick() }, - modifier = Modifier.graphicsLayer { this.alpha = alpha.value } - ) - } - } - } + scope.card() } else { Box(contentAlignment = Alignment.Center) { content() diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultMorphCard.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultMorphCard.kt new file mode 100644 index 0000000..5e691ce --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultMorphCard.kt @@ -0,0 +1,72 @@ +package com.developerstring.jetco.ui.components.button.fab.base + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig +import com.developerstring.jetco.ui.components.button.fab.scope.MorphCardScope + +@OptIn(ExperimentalLayoutApi::class) +@Composable +internal fun DefaultMorphCard( + config: FabMainConfig, + onClose: () -> Unit, + scope: MorphCardScope +) { + val morph = config.itemArrangement.morph + + Column( + modifier = Modifier + .width(morph.width) + .background( + color = config.buttonStyle.color, + shape = morph.cardShape + ).padding(16.dp) + ) { + // header + Box(modifier = Modifier.fillMaxWidth()) { + Box(modifier = Modifier.align(Alignment.CenterStart)) { + Text( + text = "Quick Actions", + color = Color.White + ) + } + Box( + modifier = Modifier + .size(32.dp) + .align(Alignment.CenterEnd) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClose + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = "Close", + tint = Color.White, + modifier = Modifier.size(20.dp) + ) + } + } + scope.MorphItems() + } +} \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/scope/MorphCardScope.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/scope/MorphCardScope.kt new file mode 100644 index 0000000..6689d53 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/scope/MorphCardScope.kt @@ -0,0 +1,10 @@ +package com.developerstring.jetco.ui.components.button.fab.scope + +import androidx.compose.runtime.Composable + +class MorphCardScope( + internal val itemsContent: @Composable () -> Unit +) { + @Composable + fun MorphItems() = itemsContent() +} \ No newline at end of file From 9c9c1c81f04302d9d7eddc7ec429548d5baa6f61 Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Sat, 28 Mar 2026 12:39:56 -0300 Subject: [PATCH 13/19] feat(fab): add animation for main FAB interactions --- .../jetco/ui/components/button/fab/Helper.kt | 19 +++ .../button/fab/MorphFloatingActionButton.kt | 44 +++++- .../button/fab/RadialFloatingActionButton.kt | 114 +++++++++++--- .../button/fab/StackFloatingActionButton.kt | 141 +++++++++++++----- .../fab/base/DefaultFloatingActionButton.kt | 37 +---- .../button/fab/model/FabButtonTransition.kt | 70 +++++++++ .../button/fab/model/FabItemTransition.kt | 72 +++++++++ .../button/fab/model/FabMainConfig.kt | 38 +++-- .../button/fab/model/FabTransition.kt | 53 ------- .../button/fab/model/OffsetTransition.kt | 10 ++ .../button/fab/model/RotateTransition.kt | 8 + .../button/fab/model/ScaleTransition.kt | 8 + .../button/fab/model/SpringTransition.kt | 7 + 13 files changed, 449 insertions(+), 172 deletions(-) create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/Helper.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabButtonTransition.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItemTransition.kt delete mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabTransition.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/OffsetTransition.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RotateTransition.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/ScaleTransition.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/SpringTransition.kt diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/Helper.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/Helper.kt new file mode 100644 index 0000000..a1dd79e --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/Helper.kt @@ -0,0 +1,19 @@ +package com.developerstring.jetco.ui.components.button.fab + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.AnimationVector + +suspend fun Animatable.animateOrSnap( + targetValue: T?, + spec: AnimationSpec?, + predicate: () -> Boolean = { true } +) { + if (targetValue == null) return + + if (spec != null) { + animateTo(targetValue, spec) + } else if (predicate.invoke()) { + snapTo(targetValue) + } +} \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt index a69cd26..a5f3f58 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt @@ -23,6 +23,7 @@ import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem import com.developerstring.jetco.ui.components.button.fab.scope.MorphCardScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -60,7 +61,7 @@ fun MorphFloatingActionButton( onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), content: (@Composable () -> Unit) = { - DefaultFloatingActionButton(expanded = false, onClick = onClick, config = config) + DefaultFloatingActionButton(onClick = onClick, config = config) }, card: @Composable MorphCardScope.() -> Unit = { DefaultMorphCard(config = config, onClose = onClick, scope = this) @@ -79,6 +80,8 @@ fun MorphFloatingActionButton( items.forEachIndexed { index, item -> // key = index ensures remember is stable per item slot val alpha = remember(index) { Animatable(0f) } + val scale = remember(index) { Animatable(0f) } + val rotation = remember { Animatable(0f) } LaunchedEffect(index) { val transition = config.animation.enterTransition @@ -91,17 +94,46 @@ fun MorphFloatingActionButton( delay(staggerDelay) - if (transition.alphaSpec != null) { - launch { alpha.animateTo(1f, transition.alphaSpec) } - } else { - alpha.snapTo(1f) + coroutineScope { + launch { + alpha.animateOrSnap( + targetValue = if (expanded) 1f else 0f, + spec = transition.alphaSpec, + predicate = { expanded } + ) + } + launch { + scale.animateOrSnap( + targetValue = if (expanded) 1f else 0f, + spec = transition.scaleSpec, + predicate = { expanded } + ) + } + launch { + rotation.animateOrSnap( + targetValue = transition.rotate?.target, + spec = transition.rotate?.spec, + predicate = { expanded } + ) + } + } + + if (!expanded) { // Reset to initial position + alpha.snapTo(0f) + scale.snapTo(0f) + rotation.snapTo(0f) } } SubFabItem( item = item, onClick = { item.onClick() }, - modifier = Modifier.graphicsLayer { this.alpha = alpha.value } + modifier = Modifier.graphicsLayer { + this.alpha = alpha.value + this.scaleX = scale.value + this.scaleY = scale.value + this.rotationZ = rotation.value + } ) } } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt index 541bd1c..6e949d8 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt @@ -16,6 +16,7 @@ import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingAc import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.cos @@ -54,16 +55,15 @@ fun RadialFloatingActionButton( onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), content: (@Composable () -> Unit) = { - DefaultFloatingActionButton( - expanded = expanded, - onClick = onClick, - config = config - ) + DefaultFloatingActionButton(onClick = onClick, config = config) } ) { Box( modifier = modifier ) { + val fabOffsetX = remember { Animatable(0.dp, Dp.VectorConverter) } + val fabOffsetY = remember { Animatable(0.dp, Dp.VectorConverter) } + // Sub-items — laid out behind the main FAB items.forEachIndexed { index, item -> val startAngle = config.itemArrangement.radial.arc.start @@ -76,9 +76,11 @@ fun RadialFloatingActionButton( } val angleRad = Math.toRadians(angleDeg) - val targetOffsetX = if (expanded) (config.itemArrangement.radial.radius.value * cos(angleRad)).dp else 0.dp - val targetOffsetY = if (expanded) (config.itemArrangement.radial.radius.value * sin(angleRad)).dp else 0.dp + val targetOffsetX = (config.itemArrangement.radial.radius.value * cos(angleRad)).dp + val targetOffsetY = (config.itemArrangement.radial.radius.value * sin(angleRad)).dp + val rotation = remember { Animatable(0f) } + val scale = remember { Animatable(0f) } val alpha = remember { Animatable(0f) } val offsetX = remember { Animatable(0.dp, Dp.VectorConverter) } val offsetY = remember { Animatable(0.dp, Dp.VectorConverter) } @@ -91,37 +93,101 @@ fun RadialFloatingActionButton( delay(staggerDelay) - val targetX = if (expanded) targetOffsetX else targetOffsetX - val targetY = if (expanded) targetOffsetY else targetOffsetY - val targetAlpha = if (expanded) 1f else 0f - - if (transition.offsetSpec != null) { - launch { offsetX.animateTo(targetX, transition.offsetSpec) } - launch { offsetY.animateTo(targetY, transition.offsetSpec) } - } else if (expanded) { - offsetX.snapTo(targetX) - offsetY.snapTo(targetY) + coroutineScope { + launch { + offsetX.animateOrSnap( + targetValue = if (expanded) targetOffsetX else fabOffsetX.value, + spec = transition.offsetSpec, + predicate = { expanded } + ) + } + launch { + offsetY.animateOrSnap( + targetValue = if (expanded) targetOffsetY else -fabOffsetY.value, + spec = transition.offsetSpec, + predicate = { expanded } + ) + } + launch { + alpha.animateOrSnap( + targetValue = if (expanded) 1f else 0f, + spec = transition.alphaSpec, + predicate = { expanded } + ) + } + launch { + scale.animateOrSnap( + targetValue = if (expanded) 1f else 0f, + spec = transition.scaleSpec, + predicate = { expanded } + ) + } + launch { + rotation.animateOrSnap( + targetValue = transition.rotate?.target, + spec = transition.rotate?.spec, + predicate = { expanded } + ) + } } - if (transition.alphaSpec != null) { - launch { alpha.animateTo(targetAlpha, transition.alphaSpec) } - } else { - alpha.snapTo(targetAlpha) + if (!expanded) { // Reset to initial position + offsetX.snapTo(0.dp) + offsetY.snapTo(0.dp) + alpha.snapTo(0f) + scale.snapTo(0f) + rotation.snapTo(0f) } } SubFabItem( item = item, modifier = Modifier - .offset(x = offsetX.value, y = -offsetY.value) + .offset(x = offsetX.value + fabOffsetX.value, y = -offsetY.value + fabOffsetY.value) .padding(end = (config.buttonStyle.size - item.buttonStyle.size) / 2) - .graphicsLayer { this.alpha = alpha.value }, + .graphicsLayer { + this.alpha = alpha.value + this.scaleX = scale.value + this.scaleY = scale.value + this.rotationZ = rotation.value + }, onClick = { item.onClick() } ) } + val fabScale = remember { Animatable(1f) } + val fabRotation = remember { Animatable(0f) } + + LaunchedEffect(expanded) { + val btnTransition = if (expanded) config.animation.buttonEnterTransition + else config.animation.buttonExitTransition + + coroutineScope { + launch { + fabOffsetX.animateOrSnap(btnTransition.offset?.offsetX, btnTransition.offset?.spec) + } + launch { + fabOffsetY.animateOrSnap(btnTransition.offset?.offsetY, btnTransition.offset?.spec) + } + launch { + fabScale.animateOrSnap(btnTransition.scale?.target, btnTransition.scale?.spec) + } + launch { + fabRotation.animateOrSnap(btnTransition.rotation?.target, btnTransition.rotation?.spec) + } + } + } + // Main FAB button - Box { + Box( + modifier = Modifier + .offset(x = fabOffsetX.value, y = fabOffsetY.value) + .graphicsLayer { + scaleX = fabScale.value + scaleY = fabScale.value + rotationZ = fabRotation.value + } + ) { content() } } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt index b226e27..844a35d 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt @@ -23,6 +23,7 @@ import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingAc import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -58,53 +59,52 @@ fun StackFloatingActionButton( onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), content: (@Composable () -> Unit) = { - DefaultFloatingActionButton( - expanded = expanded, - onClick = onClick, - config = config - ) + DefaultFloatingActionButton(onClick = onClick, config = config) } ) { - val stack = config.itemArrangement.stack + val direction = config.itemArrangement.stack.direction val density = LocalDensity.current var fabWidthDp by remember { mutableStateOf(config.buttonStyle.size) } var fabHeightDp by remember { mutableStateOf(config.buttonStyle.size) } - val spacedBy = stack.spacedBy + val spacedBy = config.itemArrangement.stack.spacedBy // The Box anchor changes depending on which direction items spread - val alignment = when (stack) { - FabMainConfig.Orientation.Stack.TOP -> Alignment.BottomEnd - FabMainConfig.Orientation.Stack.START -> Alignment.CenterEnd - FabMainConfig.Orientation.Stack.END -> Alignment.CenterStart + val alignment = when (direction) { + FabMainConfig.Orientation.Stack.Direction.TOP -> Alignment.BottomEnd + FabMainConfig.Orientation.Stack.Direction.START -> Alignment.CenterEnd + FabMainConfig.Orientation.Stack.Direction.END -> Alignment.CenterStart } Box( modifier = modifier, contentAlignment = alignment ) { + val fabOffsetX = remember { Animatable(0.dp, Dp.VectorConverter) } + val fabOffsetY = remember { Animatable(0.dp, Dp.VectorConverter) } // Sub-items — stacked in the direction defined by Orientation.Stack items.forEachIndexed { index, item -> - - val spacing = when (stack) { - FabMainConfig.Orientation.Stack.TOP -> + val spacing = when (direction) { + FabMainConfig.Orientation.Stack.Direction.TOP -> fabHeightDp + spacedBy + index * (item.buttonStyle.size + spacedBy) else -> fabWidthDp + spacedBy + index * (item.buttonStyle.size + spacedBy) } // Each orientation moves items along a different axis - val targetOffsetX = when (stack) { - FabMainConfig.Orientation.Stack.START -> -spacing - FabMainConfig.Orientation.Stack.END -> spacing + val targetOffsetX = when (direction) { + FabMainConfig.Orientation.Stack.Direction.START -> -spacing + FabMainConfig.Orientation.Stack.Direction.END -> spacing else -> 0.dp } - val targetOffsetY = when (stack) { - FabMainConfig.Orientation.Stack.TOP -> -spacing + val targetOffsetY = when (direction) { + FabMainConfig.Orientation.Stack.Direction.TOP -> -spacing else -> 0.dp } + val rotation = remember { Animatable(0f) } + val scale = remember { Animatable(0f) } val alpha = remember { Animatable(0f) } val offsetX = remember { Animatable(0.dp, Dp.VectorConverter) } val offsetY = remember { Animatable(0.dp, Dp.VectorConverter) } @@ -117,44 +117,105 @@ fun StackFloatingActionButton( delay(staggerDelay) - val targetX = if (expanded) targetOffsetX else 0.dp - val targetY = if (expanded) targetOffsetY else 0.dp - val targetAlpha = if (expanded) 1f else 0f - - if (transition.offsetSpec != null) { - launch { offsetX.animateTo(targetX, transition.offsetSpec) } - launch { offsetY.animateTo(targetY, transition.offsetSpec) } - } else if (expanded) { - offsetX.snapTo(targetX) - offsetY.snapTo(targetY) + coroutineScope { + launch { + offsetX.animateOrSnap( + targetValue = if (expanded) targetOffsetX else 0.dp, + spec = transition.offsetSpec, + predicate = { expanded } + ) + } + launch { + offsetY.animateOrSnap( + targetValue = if (expanded) targetOffsetY else 0.dp, + spec = transition.offsetSpec, + predicate = { expanded } + ) + } + launch { + alpha.animateOrSnap( + targetValue = if (expanded) 1f else 0f, + spec = transition.alphaSpec, + predicate = { expanded } + ) + } + launch { + scale.animateOrSnap( + targetValue = if (expanded) 1f else 0f, + spec = transition.scaleSpec, + predicate = { expanded } + ) + } + launch { + rotation.animateOrSnap( + targetValue = transition.rotate?.target, + spec = transition.rotate?.spec, + predicate = { expanded } + ) + } } - if (transition.alphaSpec != null) { - launch { alpha.animateTo(targetAlpha, transition.alphaSpec) } - } else { - alpha.snapTo(targetAlpha) + if (!expanded) { // Reset to initial position + offsetX.snapTo(0.dp) + offsetY.snapTo(0.dp) + alpha.snapTo(0f) + scale.snapTo(0f) } } SubFabItem( item = item, modifier = Modifier - .offset(x = offsetX.value, y = offsetY.value) + .offset(x = offsetX.value + fabOffsetX.value, y = offsetY.value + fabOffsetY.value) .padding( - end = if (stack == FabMainConfig.Orientation.Stack.TOP) { + end = if (direction == FabMainConfig.Orientation.Stack.Direction.TOP) { (fabWidthDp - item.buttonStyle.size) / 2 } else 0.dp - ).graphicsLayer { this.alpha = alpha.value }, + ).graphicsLayer { + this.alpha = alpha.value + this.scaleX = scale.value + this.scaleY = scale.value + this.rotationZ = rotation.value + }, onClick = { item.onClick() } ) } + val fabScale = remember { Animatable(1f) } + val fabRotation = remember { Animatable(0f) } + + LaunchedEffect(expanded) { + val btnTransition = if (expanded) config.animation.buttonEnterTransition + else config.animation.buttonExitTransition + + coroutineScope { + launch { + fabOffsetX.animateOrSnap(btnTransition.offset?.offsetX, btnTransition.offset?.spec) + } + launch { + fabOffsetY.animateOrSnap(btnTransition.offset?.offsetY, btnTransition.offset?.spec) + } + launch { + fabScale.animateOrSnap(btnTransition.scale?.target, btnTransition.scale?.spec) + } + launch { + fabRotation.animateOrSnap(btnTransition.rotation?.target, btnTransition.rotation?.spec) + } + } + } + // Main FAB Box( - modifier = Modifier.onSizeChanged { size -> - fabWidthDp = with(density) { size.width.toDp() } - fabHeightDp = with(density) { size.height.toDp() } - } + modifier = Modifier + .offset(x = fabOffsetX.value, y = fabOffsetY.value) + .graphicsLayer { + scaleX = fabScale.value + scaleY = fabScale.value + rotationZ = fabRotation.value + }.onSizeChanged { size -> + fabWidthDp = with(density) { size.width.toDp() } + fabHeightDp = with(density) { size.height.toDp() } + } ) { content() } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFloatingActionButton.kt index 946b853..b1b9081 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFloatingActionButton.kt @@ -1,44 +1,30 @@ package com.developerstring.jetco.ui.components.button.fab.base -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add import androidx.compose.material3.Icon import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig @Composable internal fun DefaultFloatingActionButton( - expanded: Boolean, modifier: Modifier = Modifier, onClick: (() -> Unit) = {}, config: FabMainConfig = FabMainConfig() ) { - // Rotate the main icon smoothly when toggling open/close - val mainIconRotation by animateFloatAsState( - targetValue = if (expanded) config.buttonStyle.iconRotation else 0f, - animationSpec = config.animation.animationSpec, - label = "mainIconRotation" - ) - Box( modifier = modifier .defaultMinSize(minWidth = config.buttonStyle.size) @@ -51,22 +37,15 @@ internal fun DefaultFloatingActionButton( ) { onClick.invoke() }, contentAlignment = Alignment.Center ) { - Row( - horizontalArrangement = Arrangement.spacedBy(config.buttonStyle.horizontalSpace), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(config.buttonStyle.padding) + Box( + contentAlignment = Alignment.Center, ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.rotate(mainIconRotation) - ) { - Icon( - imageVector = Icons.Rounded.Add, - contentDescription = "Base FAB icon", - tint = Color.White, - modifier = Modifier.size(config.buttonStyle.size * 0.55f) - ) - } + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = "Base FAB icon", + tint = Color.White, + modifier = Modifier.size(config.buttonStyle.size * 0.55f) + ) } } } \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabButtonTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabButtonTransition.kt new file mode 100644 index 0000000..8bba011 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabButtonTransition.kt @@ -0,0 +1,70 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +class FabButtonTransition internal constructor( + val offset: OffsetTransition? = null, + val scale: ScaleTransition? = null, + val rotation: RotateTransition? = null, + val spring: SpringTransition? = null +) { + operator fun plus(other: FabButtonTransition): FabButtonTransition = FabButtonTransition( + offset = other.offset ?: this.offset, + scale = other.scale ?: this.scale, + rotation = other.rotation ?: this.rotation, + spring = other.spring ?: this.spring + ) + + companion object { + + fun SlideTo( + x: Dp = 0.dp, + y: Dp = 0.dp, + durationMillis: Int = 300, + easing: Easing = FastOutSlowInEasing + ): FabButtonTransition = FabButtonTransition( + offset = OffsetTransition( + offsetX = x, + offsetY = y, + spec = tween(durationMillis, easing = easing) + ) + ) + + fun Scale( + scale: Float, + durationMillis: Int = 300, + easing: Easing = FastOutSlowInEasing + ): FabButtonTransition = FabButtonTransition( + scale = ScaleTransition( + target = scale, + spec = tween(durationMillis, easing = easing) + ) + ) + + fun Rotate( + rotation: Float, + durationMillis: Int = 300, + easing: Easing = FastOutSlowInEasing + ): FabButtonTransition = FabButtonTransition( + rotation = RotateTransition( + target = rotation, + spec = tween(durationMillis, easing = easing) + ) + ) + + fun Spring( + dampingRatio: Float = Spring.DampingRatioMediumBouncy, + stiffness: Float = Spring.StiffnessMedium + ): FabButtonTransition = FabButtonTransition( + spring = SpringTransition( + spec = spring(dampingRatio, stiffness) + ) + ) + } +} \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItemTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItemTransition.kt new file mode 100644 index 0000000..c49970d --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItemTransition.kt @@ -0,0 +1,72 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.ui.unit.Dp + +class FabItemTransition( + val offsetSpec: AnimationSpec? = null, + val alphaSpec: AnimationSpec? = null, + val scaleSpec: AnimationSpec? = null, + val rotate: RotateTransition? = null +) { + operator fun plus(other: FabItemTransition): FabItemTransition = FabItemTransition( + offsetSpec = other.offsetSpec ?: this.offsetSpec, + alphaSpec = other.alphaSpec ?: this.alphaSpec, + scaleSpec = other.scaleSpec ?: this.scaleSpec, + rotate = other.rotate ?: this.rotate + ) + + companion object { + fun Spring( + dampingRatio: Float = Spring.DampingRatioMediumBouncy, + stiffness: Float = Spring.StiffnessMedium + ): FabItemTransition = FabItemTransition( + offsetSpec = spring(dampingRatio, stiffness) + ) + + fun Slide( + durationMillis: Int = 300, + easing: Easing = FastOutSlowInEasing + ): FabItemTransition = FabItemTransition( + offsetSpec = tween(durationMillis, easing = easing) + ) + + fun Fade( + durationMillis: Int = 300, + easing: Easing = FastOutSlowInEasing + ): FabItemTransition = FabItemTransition( + alphaSpec = tween(durationMillis, easing = easing), + ) + + fun SlideAndFade( + durationMillis: Int = 300, + easing: Easing = FastOutSlowInEasing + ): FabItemTransition = FabItemTransition( + offsetSpec = tween(durationMillis, easing = easing), + alphaSpec = tween(durationMillis, easing = easing), + ) + + fun Scale( + durationMillis: Int = 300, + easing: Easing = FastOutSlowInEasing + ): FabItemTransition = FabItemTransition( + scaleSpec = tween(durationMillis, easing = easing) + ) + + fun Rotate( + target: Float, + durationMillis: Int = 300, + easing: Easing = FastOutSlowInEasing + ): FabItemTransition = FabItemTransition( + rotate = RotateTransition( + target = target, + spec = tween(durationMillis, easing = easing) + ) + ) + } +} \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt index 9e388c7..fa203b5 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt @@ -1,11 +1,7 @@ package com.developerstring.jetco.ui.components.button.fab.model import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.Easing import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -40,7 +36,7 @@ data class FabMainConfig( val arc: Arc = Arc.END, val radius: Dp = 80.dp ) : Orientation { - enum class Arc(val start: Double, val end: Double) : Orientation { + enum class Arc(val start: Double, val end: Double) { /** Spreads items from 90° to 180° (upward and to the left). */ END(90.0, 180.0), /** Spreads items from 90° to 0° (upward and to the right). */ @@ -50,18 +46,18 @@ data class FabMainConfig( } } - /** - * Stack orientation that spreads sub-items linearly in one direction. - * - * @property spacedBy Gap between each sub-item. Default is 40.dp. - */ - enum class Stack(val spacedBy: Dp = 40.dp) : Orientation { - /** Spreads items upward above the main FAB. */ - TOP, - /** Spreads items to the left of the main FAB. */ - START, - /** Spreads items to the right of the main FAB. */ - END + data class Stack( + val direction: Direction = Direction.TOP, + val spacedBy: Dp = 40.dp + ) : Orientation { + enum class Direction { + /** Spreads items upward above the main FAB. */ + TOP, + /** Spreads items to the left of the main FAB. */ + START, + /** Spreads items to the right of the main FAB. */ + END + } } /** @@ -93,8 +89,10 @@ data class FabMainConfig( open class Animation( val enterOrder: StaggerOrder = StaggerOrder.FIFO, val exitOrder: StaggerOrder = StaggerOrder.FILO, - val enterTransition: FabTransition = FabTransition.Spring() + FabTransition.Fade(), - val exitTransition: FabTransition = FabTransition.Slide() + FabTransition.Fade() + val enterTransition: FabItemTransition = FabItemTransition.Spring() + FabItemTransition.Fade(), + val exitTransition: FabItemTransition = FabItemTransition.Slide() + FabItemTransition.Fade(), + val buttonEnterTransition: FabButtonTransition = FabButtonTransition.Rotate(45f), + val buttonExitTransition: FabButtonTransition = FabButtonTransition.Rotate(0f) ) @Stable @@ -139,7 +137,7 @@ data class FabMainConfig( @Stable data class ItemArrangement( val radial: Orientation.Radial = Orientation.Radial(), - val stack: Orientation.Stack = Orientation.Stack.TOP, + val stack: Orientation.Stack = Orientation.Stack(), val morph: Orientation.Morph = Orientation.Morph() ) } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabTransition.kt deleted file mode 100644 index 8d11a55..0000000 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabTransition.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.developerstring.jetco.ui.components.button.fab.model - -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.Easing -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween -import androidx.compose.ui.unit.Dp - -class FabTransition internal constructor( - internal val offsetSpec: AnimationSpec?, - internal val alphaSpec: AnimationSpec? -) { - operator fun plus(other: FabTransition): FabTransition = FabTransition( - offsetSpec = other.offsetSpec ?: this.offsetSpec, - alphaSpec = other.alphaSpec ?: this.alphaSpec - ) - - companion object { - fun Spring( - dampingRatio: Float = Spring.DampingRatioMediumBouncy, - stiffness: Float = Spring.StiffnessMedium - ): FabTransition = FabTransition( - offsetSpec = spring(dampingRatio, stiffness), - alphaSpec = null - ) - - fun Slide( - durationMillis: Int = 300, - easing: Easing = FastOutSlowInEasing - ): FabTransition = FabTransition( - offsetSpec = tween(durationMillis, easing = easing), - alphaSpec = null - ) - - fun Fade( - durationMillis: Int = 300, - easing: Easing = FastOutSlowInEasing - ): FabTransition = FabTransition( - offsetSpec = null, - alphaSpec = tween(durationMillis, easing = easing) - ) - - fun SlideAndFade( - durationMillis: Int = 300, - easing: Easing = FastOutSlowInEasing - ): FabTransition = FabTransition( - offsetSpec = tween(durationMillis, easing = easing), - alphaSpec = tween(durationMillis, easing = easing) - ) - } -} \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/OffsetTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/OffsetTransition.kt new file mode 100644 index 0000000..1651d4d --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/OffsetTransition.kt @@ -0,0 +1,10 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.ui.unit.Dp + +data class OffsetTransition( + val offsetX: Dp, + val offsetY: Dp, + val spec: AnimationSpec +) \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RotateTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RotateTransition.kt new file mode 100644 index 0000000..d8ac81b --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RotateTransition.kt @@ -0,0 +1,8 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.animation.core.AnimationSpec + +data class RotateTransition( + val target: Float, + val spec: AnimationSpec +) \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/ScaleTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/ScaleTransition.kt new file mode 100644 index 0000000..c3eaf74 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/ScaleTransition.kt @@ -0,0 +1,8 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.animation.core.AnimationSpec + +data class ScaleTransition( + val target: Float, + val spec: AnimationSpec +) \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/SpringTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/SpringTransition.kt new file mode 100644 index 0000000..4e89d12 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/SpringTransition.kt @@ -0,0 +1,7 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.animation.core.AnimationSpec + +data class SpringTransition( + val spec: AnimationSpec +) \ No newline at end of file From cf1cb6a03b2067cf60edf7f0277554c9ac79c0db Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Sat, 28 Mar 2026 17:49:41 -0300 Subject: [PATCH 14/19] feat(fab): add support for custom composable items --- .../jetco/ui/components/button/fab/Helper.kt | 2 +- .../button/fab/MorphFloatingActionButton.kt | 102 ++++++++++++++---- .../button/fab/RadialFloatingActionButton.kt | 87 ++++++++++++--- .../FabSubItem.kt => base/DefaultFabItem.kt} | 8 +- .../fab/model/{FabSubItem.kt => FabItem.kt} | 4 +- 5 files changed, 157 insertions(+), 46 deletions(-) rename jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/{components/FabSubItem.kt => base/DefaultFabItem.kt} (94%) rename jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/{FabSubItem.kt => FabItem.kt} (95%) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/Helper.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/Helper.kt index a1dd79e..5d2d0f6 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/Helper.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/Helper.kt @@ -4,7 +4,7 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationVector -suspend fun Animatable.animateOrSnap( +internal suspend fun Animatable.animateOrSnap( targetValue: T?, spec: AnimationSpec?, predicate: () -> Boolean = { true } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt index a5f3f58..3c9cd99 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt @@ -1,9 +1,7 @@ package com.developerstring.jetco.ui.components.button.fab -import androidx.compose.animation.animateContentSize +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -17,11 +15,11 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer +import com.developerstring.jetco.ui.components.button.fab.base.DefaultFabItem import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingActionButton import com.developerstring.jetco.ui.components.button.fab.base.DefaultMorphCard -import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem +import com.developerstring.jetco.ui.components.button.fab.model.FabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig -import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem import com.developerstring.jetco.ui.components.button.fab.scope.MorphCardScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay @@ -47,7 +45,7 @@ import kotlinx.coroutines.launch * ``` * * @param expanded Whether the FAB is currently expanded into card form. - * @param items List of [FabSubItem] sub-actions to display in the card grid. + * @param items List of [FabItem] sub-actions to display in the card grid. * @param modifier Modifier applied to the root [Box] container. * @param onClick Click handler for both the main FAB button and the card close button. * @param config Visual and layout configuration. See [FabMainConfig]. @@ -56,7 +54,7 @@ import kotlinx.coroutines.launch @Composable fun MorphFloatingActionButton( expanded: Boolean, - items: List, + items: List, modifier: Modifier = Modifier, onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), @@ -66,6 +64,68 @@ fun MorphFloatingActionButton( card: @Composable MorphCardScope.() -> Unit = { DefaultMorphCard(config = config, onClose = onClick, scope = this) } +) { + MorphFloatingActionButtonBase( + expanded = expanded, + itemCount = items.size, + modifier = modifier, + onClick = onClick, + config = config, + content = content, + card = card, + itemContent = { index -> + val item = items[index] + + DefaultFabItem( + item = item, + onClick = { item.onClick() }, + ) + } + ) +} + +@JvmName("MorphFloatingActionButtonCustom") +@Composable +fun MorphFloatingActionButton( + expanded: Boolean, + items: List<@Composable () -> Unit>, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + config: FabMainConfig = FabMainConfig(), + content: (@Composable () -> Unit) = { + DefaultFloatingActionButton(onClick = onClick, config = config) + }, + card: @Composable MorphCardScope.() -> Unit = { + DefaultMorphCard(config = config, onClose = onClick, scope = this) + } +) { + MorphFloatingActionButtonBase( + expanded = expanded, + itemCount = items.size, + modifier = modifier, + onClick = onClick, + config = config, + content = content, + card = card, + itemContent = { items[it]() } + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +internal fun MorphFloatingActionButtonBase( + expanded: Boolean, + itemCount: Int, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + config: FabMainConfig = FabMainConfig(), + content: (@Composable () -> Unit) = { + DefaultFloatingActionButton(onClick = onClick, config = config) + }, + card: @Composable MorphCardScope.() -> Unit = { + DefaultMorphCard(config = config, onClose = onClick, scope = this) + }, + itemContent: @Composable (index: Int) -> Unit ) { val morph = config.itemArrangement.morph val itemsContent: @Composable () -> Unit = { @@ -77,7 +137,7 @@ fun MorphFloatingActionButton( verticalArrangement = Arrangement.spacedBy(morph.spacedBy), modifier = Modifier.fillMaxWidth() ) { - items.forEachIndexed { index, item -> + repeat(itemCount) { index -> // key = index ensures remember is stable per item slot val alpha = remember(index) { Animatable(0f) } val scale = remember(index) { Animatable(0f) } @@ -85,10 +145,10 @@ fun MorphFloatingActionButton( LaunchedEffect(index) { val transition = config.animation.enterTransition - val stepMs = 300 / (items.size + 1) + val stepMs = 300 / itemCount val staggerDelay = config.animation.enterOrder.delayFor( index = index, - total = items.size, + total = itemCount - 1, stepMs = stepMs ) @@ -125,16 +185,16 @@ fun MorphFloatingActionButton( } } - SubFabItem( - item = item, - onClick = { item.onClick() }, + Box( modifier = Modifier.graphicsLayer { this.alpha = alpha.value this.scaleX = scale.value this.scaleY = scale.value this.rotationZ = rotation.value } - ) + ) { + itemContent(index) + } } } } @@ -142,19 +202,15 @@ fun MorphFloatingActionButton( itemsContent = itemsContent ) - Box( + AnimatedContent( + targetState = expanded, + label = "MorphFloatingActionButton", modifier = modifier - .animateContentSize( - animationSpec = tween( - durationMillis = 300, - easing = FastOutSlowInEasing - ) - ) ) { - if (expanded) { + if (it) { scope.card() } else { - Box(contentAlignment = Alignment.Center) { + Box { content() } } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt index 6e949d8..770d24e 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt @@ -8,14 +8,15 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.developerstring.jetco.ui.components.button.fab.base.DefaultFabItem import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingActionButton -import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig -import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem +import com.developerstring.jetco.ui.components.button.fab.model.FabItem import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -42,7 +43,7 @@ import kotlin.math.sin * ``` * * @param expanded Whether the FAB is currently expanded, showing sub-items. - * @param items List of [FabSubItem] sub-actions to display when expanded. + * @param items List of [FabItem] sub-actions to display when expanded. * @param modifier Modifier applied to the root [Box] container. * @param onClick Click handler for the main FAB button. * @param config Visual and layout configuration. See [FabMainConfig]. @@ -50,29 +51,84 @@ import kotlin.math.sin @Composable fun RadialFloatingActionButton( expanded: Boolean, - items: List, + items: List, modifier: Modifier = Modifier, onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), content: (@Composable () -> Unit) = { DefaultFloatingActionButton(onClick = onClick, config = config) } +) { + RadialFloatingActionButtonBase( + expanded = expanded, + itemCount = items.size, + modifier = modifier, + onClick = onClick, + config = config, + content = content, + itemContent = { index -> + val item = items[index] + + DefaultFabItem( + item = item, + modifier = Modifier.padding(end = (config.buttonStyle.size - item.buttonStyle.size) / 2), + onClick = { item.onClick() } + ) + } + ) +} + +@JvmName("RadialFloatingActionButtonCustom") +@Composable +fun RadialFloatingActionButton( + expanded: Boolean, + items: List<@Composable () -> Unit>, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + config: FabMainConfig = FabMainConfig(), + content: (@Composable () -> Unit) = { + DefaultFloatingActionButton(onClick = onClick, config = config) + } +) { + RadialFloatingActionButtonBase( + expanded = expanded, + itemCount = items.size, + modifier = modifier, + onClick = onClick, + config = config, + content = content, + itemContent = { items[it]() } + ) +} + +@Composable +internal fun RadialFloatingActionButtonBase( + expanded: Boolean, + itemCount: Int, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + config: FabMainConfig = FabMainConfig(), + content: (@Composable () -> Unit) = { + DefaultFloatingActionButton(onClick = onClick, config = config) + }, + itemContent: @Composable (index: Int) -> Unit ) { Box( - modifier = modifier + modifier = modifier, + contentAlignment = Alignment.BottomEnd ) { val fabOffsetX = remember { Animatable(0.dp, Dp.VectorConverter) } val fabOffsetY = remember { Animatable(0.dp, Dp.VectorConverter) } // Sub-items — laid out behind the main FAB - items.forEachIndexed { index, item -> + repeat(itemCount) { index -> val startAngle = config.itemArrangement.radial.arc.start val endAngle = config.itemArrangement.radial.arc.end - val angleDeg = if (items.size == 1) { + val angleDeg = if (itemCount == 1) { (startAngle + endAngle) / 2.0 // single item lands at the midpoint of the arc } else { - startAngle + (endAngle - startAngle) * (index.toDouble() / items.lastIndex) + startAngle + (endAngle - startAngle) * (index.toDouble() / (itemCount - 1)) } val angleRad = Math.toRadians(angleDeg) @@ -86,10 +142,10 @@ fun RadialFloatingActionButton( val offsetY = remember { Animatable(0.dp, Dp.VectorConverter) } LaunchedEffect(expanded) { - val stepMs = 300 / (items.size + 1) + val stepMs = 300 / itemCount val order = if (expanded) config.animation.enterOrder else config.animation.exitOrder val transition = if (expanded) config.animation.enterTransition else config.animation.exitTransition - val staggerDelay = order.delayFor(index = index, total = items.size, stepMs = stepMs) + val staggerDelay = order.delayFor(index = index, total = (itemCount - 1), stepMs = stepMs) delay(staggerDelay) @@ -140,19 +196,18 @@ fun RadialFloatingActionButton( } } - SubFabItem( - item = item, + Box( modifier = Modifier .offset(x = offsetX.value + fabOffsetX.value, y = -offsetY.value + fabOffsetY.value) - .padding(end = (config.buttonStyle.size - item.buttonStyle.size) / 2) .graphicsLayer { this.alpha = alpha.value this.scaleX = scale.value this.scaleY = scale.value this.rotationZ = rotation.value - }, - onClick = { item.onClick() } - ) + } + ) { + itemContent(index) + } } val fabScale = remember { Animatable(1f) } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/components/FabSubItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFabItem.kt similarity index 94% rename from jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/components/FabSubItem.kt rename to jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFabItem.kt index cc1a6af..c1042f7 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/components/FabSubItem.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFabItem.kt @@ -1,4 +1,4 @@ -package com.developerstring.jetco.ui.components.button.fab.components +package com.developerstring.jetco.ui.components.button.fab.base import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background @@ -18,11 +18,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem +import com.developerstring.jetco.ui.components.button.fab.model.FabItem @Composable -internal fun SubFabItem( - item: FabSubItem, +internal fun DefaultFabItem( + item: FabItem, modifier: Modifier = Modifier, onClick: () -> Unit ) { diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt similarity index 95% rename from jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt rename to jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt index 5b6c3e5..f08a1dc 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabSubItem.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp /** * Data model representing a single sub-action item displayed by a FAB variant. * - * Each [FabSubItem] carries an action callback, an optional icon, an optional title, + * Each [FabItem] carries an action callback, an optional icon, an optional title, * and independent style configurations for both the button and the title label. * The title is only rendered when the FAB variant explicitly enables it * @@ -24,7 +24,7 @@ import androidx.compose.ui.unit.dp * @param titleStyle Visual style of the title label. See [TitleStyle]. */ @Stable -data class FabSubItem( +data class FabItem( val onClick: () -> Unit, val title: String? = null, val icon: ImageVector? = null, From bf0b24ccefbd320a7e48bb76d2600cb37a6b3024 Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Sun, 29 Mar 2026 13:35:00 -0300 Subject: [PATCH 15/19] feat(fab): modularize transitions and refine stack behavior --- .../button/fab/MorphFloatingActionButton.kt | 39 ++-- .../button/fab/RadialFloatingActionButton.kt | 47 ++--- .../button/fab/StackFloatingActionButton.kt | 181 ++++++++++++------ .../ui/components/button/fab/model/FabItem.kt | 5 +- .../button/fab/model/FabMainConfig.kt | 14 +- .../button/fab/model/MorphFabItem.kt | 7 + .../button/fab/model/RadialFabItem.kt | 7 + .../button/fab/model/StackDirection.kt | 10 + .../button/fab/model/StackFabItem.kt | 10 + .../FabButtonTransition.kt | 2 +- .../FabItemTransition.kt | 2 +- .../{model => transition}/OffsetTransition.kt | 2 +- .../{model => transition}/RotateTransition.kt | 2 +- .../{model => transition}/ScaleTransition.kt | 2 +- .../{model => transition}/SpringTransition.kt | 2 +- 15 files changed, 205 insertions(+), 127 deletions(-) create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/MorphFabItem.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RadialFabItem.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackDirection.kt create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackFabItem.kt rename jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/{model => transition}/FabButtonTransition.kt (96%) rename jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/{model => transition}/FabItemTransition.kt (97%) rename jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/{model => transition}/OffsetTransition.kt (73%) rename jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/{model => transition}/RotateTransition.kt (67%) rename jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/{model => transition}/ScaleTransition.kt (66%) rename jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/{model => transition}/SpringTransition.kt (63%) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt index 3c9cd99..f00ea45 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt @@ -20,6 +20,7 @@ import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingAc import com.developerstring.jetco.ui.components.button.fab.base.DefaultMorphCard import com.developerstring.jetco.ui.components.button.fab.model.FabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig +import com.developerstring.jetco.ui.components.button.fab.model.MorphFabItem import com.developerstring.jetco.ui.components.button.fab.scope.MorphCardScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay @@ -65,22 +66,20 @@ fun MorphFloatingActionButton( DefaultMorphCard(config = config, onClose = onClick, scope = this) } ) { + val items = items.map { item -> + MorphFabItem { + DefaultFabItem(item = item, onClick = { item.onClick() }) + } + } + MorphFloatingActionButtonBase( expanded = expanded, - itemCount = items.size, + items = items, modifier = modifier, onClick = onClick, config = config, content = content, - card = card, - itemContent = { index -> - val item = items[index] - - DefaultFabItem( - item = item, - onClick = { item.onClick() }, - ) - } + card = card ) } @@ -88,7 +87,7 @@ fun MorphFloatingActionButton( @Composable fun MorphFloatingActionButton( expanded: Boolean, - items: List<@Composable () -> Unit>, + items: List, modifier: Modifier = Modifier, onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), @@ -101,13 +100,12 @@ fun MorphFloatingActionButton( ) { MorphFloatingActionButtonBase( expanded = expanded, - itemCount = items.size, + items = items, modifier = modifier, onClick = onClick, config = config, content = content, - card = card, - itemContent = { items[it]() } + card = card ) } @@ -115,7 +113,7 @@ fun MorphFloatingActionButton( @Composable internal fun MorphFloatingActionButtonBase( expanded: Boolean, - itemCount: Int, + items: List, modifier: Modifier = Modifier, onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), @@ -124,8 +122,7 @@ internal fun MorphFloatingActionButtonBase( }, card: @Composable MorphCardScope.() -> Unit = { DefaultMorphCard(config = config, onClose = onClick, scope = this) - }, - itemContent: @Composable (index: Int) -> Unit + } ) { val morph = config.itemArrangement.morph val itemsContent: @Composable () -> Unit = { @@ -137,7 +134,7 @@ internal fun MorphFloatingActionButtonBase( verticalArrangement = Arrangement.spacedBy(morph.spacedBy), modifier = Modifier.fillMaxWidth() ) { - repeat(itemCount) { index -> + items.forEachIndexed { index, morphItem -> // key = index ensures remember is stable per item slot val alpha = remember(index) { Animatable(0f) } val scale = remember(index) { Animatable(0f) } @@ -145,10 +142,10 @@ internal fun MorphFloatingActionButtonBase( LaunchedEffect(index) { val transition = config.animation.enterTransition - val stepMs = 300 / itemCount + val stepMs = 300 / (items.size - 1) val staggerDelay = config.animation.enterOrder.delayFor( index = index, - total = itemCount - 1, + total = items.size - 1, stepMs = stepMs ) @@ -193,7 +190,7 @@ internal fun MorphFloatingActionButtonBase( this.rotationZ = rotation.value } ) { - itemContent(index) + morphItem.content() } } } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt index 770d24e..2df6d16 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt @@ -4,7 +4,6 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -15,8 +14,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.developerstring.jetco.ui.components.button.fab.base.DefaultFabItem import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingActionButton -import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabItem +import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig +import com.developerstring.jetco.ui.components.button.fab.model.RadialFabItem import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -59,22 +59,19 @@ fun RadialFloatingActionButton( DefaultFloatingActionButton(onClick = onClick, config = config) } ) { + val items = items.map { item -> + RadialFabItem { + DefaultFabItem(item = item, onClick = { item.onClick() }) + } + } + RadialFloatingActionButtonBase( expanded = expanded, - itemCount = items.size, + items = items, modifier = modifier, onClick = onClick, config = config, - content = content, - itemContent = { index -> - val item = items[index] - - DefaultFabItem( - item = item, - modifier = Modifier.padding(end = (config.buttonStyle.size - item.buttonStyle.size) / 2), - onClick = { item.onClick() } - ) - } + content = content ) } @@ -82,7 +79,7 @@ fun RadialFloatingActionButton( @Composable fun RadialFloatingActionButton( expanded: Boolean, - items: List<@Composable () -> Unit>, + items: List, modifier: Modifier = Modifier, onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), @@ -92,26 +89,24 @@ fun RadialFloatingActionButton( ) { RadialFloatingActionButtonBase( expanded = expanded, - itemCount = items.size, + items = items, modifier = modifier, onClick = onClick, config = config, - content = content, - itemContent = { items[it]() } + content = content ) } @Composable internal fun RadialFloatingActionButtonBase( expanded: Boolean, - itemCount: Int, + items: List, modifier: Modifier = Modifier, onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), content: (@Composable () -> Unit) = { DefaultFloatingActionButton(onClick = onClick, config = config) - }, - itemContent: @Composable (index: Int) -> Unit + } ) { Box( modifier = modifier, @@ -121,14 +116,14 @@ internal fun RadialFloatingActionButtonBase( val fabOffsetY = remember { Animatable(0.dp, Dp.VectorConverter) } // Sub-items — laid out behind the main FAB - repeat(itemCount) { index -> + items.forEachIndexed { index, radialItem -> val startAngle = config.itemArrangement.radial.arc.start val endAngle = config.itemArrangement.radial.arc.end - val angleDeg = if (itemCount == 1) { + val angleDeg = if (items.size == 1) { (startAngle + endAngle) / 2.0 // single item lands at the midpoint of the arc } else { - startAngle + (endAngle - startAngle) * (index.toDouble() / (itemCount - 1)) + startAngle + (endAngle - startAngle) * (index.toDouble() / (items.size - 1)) } val angleRad = Math.toRadians(angleDeg) @@ -142,10 +137,10 @@ internal fun RadialFloatingActionButtonBase( val offsetY = remember { Animatable(0.dp, Dp.VectorConverter) } LaunchedEffect(expanded) { - val stepMs = 300 / itemCount + val stepMs = 300 / (items.size - 1) val order = if (expanded) config.animation.enterOrder else config.animation.exitOrder val transition = if (expanded) config.animation.enterTransition else config.animation.exitTransition - val staggerDelay = order.delayFor(index = index, total = (itemCount - 1), stepMs = stepMs) + val staggerDelay = order.delayFor(index = index, total = (items.size - 1), stepMs = stepMs) delay(staggerDelay) @@ -206,7 +201,7 @@ internal fun RadialFloatingActionButtonBase( this.rotationZ = rotation.value } ) { - itemContent(index) + radialItem.content() } } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt index 844a35d..4a1c028 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt @@ -4,11 +4,11 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -18,43 +18,69 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.times +import com.developerstring.jetco.ui.components.button.fab.base.DefaultFabItem import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingActionButton -import com.developerstring.jetco.ui.components.button.fab.components.SubFabItem +import com.developerstring.jetco.ui.components.button.fab.model.FabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig -import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem +import com.developerstring.jetco.ui.components.button.fab.model.StackDirection +import com.developerstring.jetco.ui.components.button.fab.model.StackFabItem import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -/** - * A Floating Action Button that expands sub-items linearly in a stack — above, to the left, - * or to the right of the main button. - * - * Sub-items are pushed outward using animated offsets driven by tween animations. - * - * ## Example Usage: - * ```kotlin - * StackFloatingActionButton( - * expanded = isExpanded, - * items = listOf( - * FabSubItem( - * onClick = { } - * ) - * ) - * ) - * ``` - * - * @param expanded Whether the FAB is currently expanded, showing sub-items. - * @param items List of [FabSubItem] sub-actions to display when expanded. - * @param modifier Modifier applied to the root [Box] container. - * @param onClick Click handler for the main FAB button. - * @param config Visual and layout configuration. See [FabMainConfig]. - */ @Composable fun StackFloatingActionButton( expanded: Boolean, - items: List, + items: List, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + config: FabMainConfig = FabMainConfig(), + content: (@Composable () -> Unit) = { + DefaultFloatingActionButton(onClick = onClick, config = config) + } +) { + val stackItems = items.map { item -> + StackFabItem { + DefaultFabItem(item = item, onClick = { item.onClick() }) + } + } + + StackFloatingActionButtonBase( + expanded = expanded, + items = stackItems, + modifier = modifier, + onClick = onClick, + config = config, + content = content + ) +} + +@JvmName("StackFloatingActionButtonCustom") +@Composable +fun StackFloatingActionButton( + expanded: Boolean, + items: List, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + config: FabMainConfig = FabMainConfig(), + content: (@Composable () -> Unit) = { + DefaultFloatingActionButton(onClick = onClick, config = config) + } +) { + StackFloatingActionButtonBase( + expanded = expanded, + items = items, + modifier = modifier, + onClick = onClick, + config = config, + content = content + ) +} + +@Composable +internal fun StackFloatingActionButtonBase( + expanded: Boolean, + items: List, modifier: Modifier = Modifier, onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), @@ -62,45 +88,69 @@ fun StackFloatingActionButton( DefaultFloatingActionButton(onClick = onClick, config = config) } ) { - val direction = config.itemArrangement.stack.direction val density = LocalDensity.current var fabWidthDp by remember { mutableStateOf(config.buttonStyle.size) } var fabHeightDp by remember { mutableStateOf(config.buttonStyle.size) } val spacedBy = config.itemArrangement.stack.spacedBy - // The Box anchor changes depending on which direction items spread - val alignment = when (direction) { - FabMainConfig.Orientation.Stack.Direction.TOP -> Alignment.BottomEnd - FabMainConfig.Orientation.Stack.Direction.START -> Alignment.CenterEnd - FabMainConfig.Orientation.Stack.Direction.END -> Alignment.CenterStart + val itemWidths = remember(items.size) { + mutableStateListOf().also { list -> + repeat(items.size) { list.add(config.buttonStyle.size) } + } + } + val itemHeights = remember(items.size) { + mutableStateListOf().also { list -> + repeat(items.size) { list.add(config.buttonStyle.size) } + } } + val topIndices = remember(items) { items.indices.filter { items[it].direction == StackDirection.TOP } } + val startIndices = remember(items) { items.indices.filter { items[it].direction == StackDirection.START } } + val endIndices = remember(items) { items.indices.filter { items[it].direction == StackDirection.END } } + Box( modifier = modifier, - contentAlignment = alignment + contentAlignment = Alignment.BottomEnd ) { val fabOffsetX = remember { Animatable(0.dp, Dp.VectorConverter) } val fabOffsetY = remember { Animatable(0.dp, Dp.VectorConverter) } - // Sub-items — stacked in the direction defined by Orientation.Stack - items.forEachIndexed { index, item -> + + items.forEachIndexed { index, stackItem -> + val direction = stackItem.direction + + val groupIndices = when (direction) { + StackDirection.TOP -> topIndices + StackDirection.START -> startIndices + StackDirection.END -> endIndices + } + val positionInGroup = groupIndices.indexOf(index) val spacing = when (direction) { - FabMainConfig.Orientation.Stack.Direction.TOP -> - fabHeightDp + spacedBy + index * (item.buttonStyle.size + spacedBy) - else -> - fabWidthDp + spacedBy + index * (item.buttonStyle.size + spacedBy) + StackDirection.TOP -> { + val heightBefore = (0 until positionInGroup).fold(0.dp) { acc, pos -> + acc + itemHeights[groupIndices[pos]] + spacedBy + } + fabHeightDp + spacedBy + heightBefore + } + StackDirection.START, + StackDirection.END -> { + val widthBefore = (0 until positionInGroup).fold(0.dp) { acc, pos -> + acc + itemWidths[groupIndices[pos]] + spacedBy + } + fabWidthDp + spacedBy + widthBefore + } } - // Each orientation moves items along a different axis val targetOffsetX = when (direction) { - FabMainConfig.Orientation.Stack.Direction.START -> -spacing - FabMainConfig.Orientation.Stack.Direction.END -> spacing - else -> 0.dp + StackDirection.START -> -spacing + StackDirection.END -> spacing + StackDirection.TOP -> -(fabWidthDp - itemWidths[index]) / 2 } val targetOffsetY = when (direction) { - FabMainConfig.Orientation.Stack.Direction.TOP -> -spacing - else -> 0.dp + StackDirection.TOP -> -spacing + StackDirection.START, + StackDirection.END -> -(fabHeightDp - itemHeights[index]) / 2 } val rotation = remember { Animatable(0f) } @@ -109,11 +159,17 @@ fun StackFloatingActionButton( val offsetX = remember { Animatable(0.dp, Dp.VectorConverter) } val offsetY = remember { Animatable(0.dp, Dp.VectorConverter) } - LaunchedEffect(expanded) { - val stepMs = 300 / (items.size + 1) + val groupSize = groupIndices.size + + LaunchedEffect(expanded, targetOffsetX, targetOffsetY) { + val stepMs = 300 / groupSize.coerceAtLeast(1) val order = if (expanded) config.animation.enterOrder else config.animation.exitOrder val transition = if (expanded) config.animation.enterTransition else config.animation.exitTransition - val staggerDelay = order.delayFor(index = index, total = items.size, stepMs = stepMs) + val staggerDelay = order.delayFor( + index = positionInGroup, + total = (groupSize - 1).coerceAtLeast(0), + stepMs = stepMs + ) delay(staggerDelay) @@ -163,22 +219,23 @@ fun StackFloatingActionButton( } } - SubFabItem( - item = item, + Box( modifier = Modifier .offset(x = offsetX.value + fabOffsetX.value, y = offsetY.value + fabOffsetY.value) - .padding( - end = if (direction == FabMainConfig.Orientation.Stack.Direction.TOP) { - (fabWidthDp - item.buttonStyle.size) / 2 - } else 0.dp - ).graphicsLayer { + .onSizeChanged { size -> + val width = with(density) { size.width.toDp() } + val height = with(density) { size.height.toDp() } + if (itemWidths[index] != width) itemWidths[index] = width + if (itemHeights[index] != height) itemHeights[index] = height + }.graphicsLayer { this.alpha = alpha.value this.scaleX = scale.value this.scaleY = scale.value this.rotationZ = rotation.value - }, - onClick = { item.onClick() } - ) + } + ) { + stackItem.content() + } } val fabScale = remember { Animatable(1f) } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt index f08a1dc..f0551d8 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt @@ -15,7 +15,10 @@ import androidx.compose.ui.unit.dp * * Each [FabItem] carries an action callback, an optional icon, an optional title, * and independent style configurations for both the button and the title label. - * The title is only rendered when the FAB variant explicitly enables it + * The title is only rendered when the FAB variant explicitly enables it. + * + * For [StackFloatingActionButton], each item can also carry a [direction] that controls + * which side of the main FAB it spreads toward. * * @param onClick Action invoked when the sub-item is clicked. * @param title Optional label displayed below the icon. diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt index fa203b5..5a59eef 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt @@ -10,6 +10,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.developerstring.jetco.ui.components.button.fab.transition.FabButtonTransition +import com.developerstring.jetco.ui.components.button.fab.transition.FabItemTransition /** * Main configuration class for all Floating Action Button variants in JetCo. @@ -47,18 +49,8 @@ data class FabMainConfig( } data class Stack( - val direction: Direction = Direction.TOP, val spacedBy: Dp = 40.dp - ) : Orientation { - enum class Direction { - /** Spreads items upward above the main FAB. */ - TOP, - /** Spreads items to the left of the main FAB. */ - START, - /** Spreads items to the right of the main FAB. */ - END - } - } + ) : Orientation /** * Morph orientation that expands the main FAB into a card grid. diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/MorphFabItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/MorphFabItem.kt new file mode 100644 index 0000000..1318dbc --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/MorphFabItem.kt @@ -0,0 +1,7 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.runtime.Composable + +class MorphFabItem( + val content: @Composable () -> Unit +) \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RadialFabItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RadialFabItem.kt new file mode 100644 index 0000000..22dc934 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RadialFabItem.kt @@ -0,0 +1,7 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.runtime.Composable + +class RadialFabItem( + val content: @Composable () -> Unit +) \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackDirection.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackDirection.kt new file mode 100644 index 0000000..7ef9f82 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackDirection.kt @@ -0,0 +1,10 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +enum class StackDirection { + /** Spreads items upward above the main FAB. */ + TOP, + /** Spreads items to the left of the main FAB. */ + START, + /** Spreads items to the right of the main FAB. */ + END +} \ No newline at end of file diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackFabItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackFabItem.kt new file mode 100644 index 0000000..3698c67 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackFabItem.kt @@ -0,0 +1,10 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable + +@Stable +class StackFabItem( + val direction: StackDirection = StackDirection.TOP, + val content: @Composable () -> Unit +) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabButtonTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabButtonTransition.kt similarity index 96% rename from jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabButtonTransition.kt rename to jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabButtonTransition.kt index 8bba011..6765168 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabButtonTransition.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabButtonTransition.kt @@ -1,4 +1,4 @@ -package com.developerstring.jetco.ui.components.button.fab.model +package com.developerstring.jetco.ui.components.button.fab.transition import androidx.compose.animation.core.Easing import androidx.compose.animation.core.FastOutSlowInEasing diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItemTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabItemTransition.kt similarity index 97% rename from jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItemTransition.kt rename to jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabItemTransition.kt index c49970d..e7e1f78 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItemTransition.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabItemTransition.kt @@ -1,4 +1,4 @@ -package com.developerstring.jetco.ui.components.button.fab.model +package com.developerstring.jetco.ui.components.button.fab.transition import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.Easing diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/OffsetTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/OffsetTransition.kt similarity index 73% rename from jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/OffsetTransition.kt rename to jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/OffsetTransition.kt index 1651d4d..f840c97 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/OffsetTransition.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/OffsetTransition.kt @@ -1,4 +1,4 @@ -package com.developerstring.jetco.ui.components.button.fab.model +package com.developerstring.jetco.ui.components.button.fab.transition import androidx.compose.animation.core.AnimationSpec import androidx.compose.ui.unit.Dp diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RotateTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/RotateTransition.kt similarity index 67% rename from jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RotateTransition.kt rename to jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/RotateTransition.kt index d8ac81b..18c5d16 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RotateTransition.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/RotateTransition.kt @@ -1,4 +1,4 @@ -package com.developerstring.jetco.ui.components.button.fab.model +package com.developerstring.jetco.ui.components.button.fab.transition import androidx.compose.animation.core.AnimationSpec diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/ScaleTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/ScaleTransition.kt similarity index 66% rename from jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/ScaleTransition.kt rename to jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/ScaleTransition.kt index c3eaf74..d926040 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/ScaleTransition.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/ScaleTransition.kt @@ -1,4 +1,4 @@ -package com.developerstring.jetco.ui.components.button.fab.model +package com.developerstring.jetco.ui.components.button.fab.transition import androidx.compose.animation.core.AnimationSpec diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/SpringTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/SpringTransition.kt similarity index 63% rename from jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/SpringTransition.kt rename to jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/SpringTransition.kt index 4e89d12..0340a0c 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/SpringTransition.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/SpringTransition.kt @@ -1,4 +1,4 @@ -package com.developerstring.jetco.ui.components.button.fab.model +package com.developerstring.jetco.ui.components.button.fab.transition import androidx.compose.animation.core.AnimationSpec From 1dcdb9dc04f778550dd704e0d88506ba154aaceb Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Sun, 29 Mar 2026 15:29:56 -0300 Subject: [PATCH 16/19] chore(fab): expand preview with new component usage examples --- .../FloatingActionButtonPreview.kt | 600 +++++++++++++++--- .../jetco_library/MainActivity.kt | 11 +- 2 files changed, 499 insertions(+), 112 deletions(-) diff --git a/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt b/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt index b5bbb17..809afbb 100644 --- a/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt +++ b/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt @@ -1,170 +1,566 @@ package com.developerstring.jetco_library +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AccountBox import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.Email +import androidx.compose.material.icons.outlined.Favorite import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.MailOutline +import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material.icons.outlined.Place +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.developerstring.jetco.ui.components.button.fab.MorphFloatingActionButton import com.developerstring.jetco.ui.components.button.fab.RadialFloatingActionButton import com.developerstring.jetco.ui.components.button.fab.StackFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.model.FabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig -import com.developerstring.jetco.ui.components.button.fab.model.FabSubItem - -private val items = listOf( - FabSubItem( - onClick = { println("handle home") }, - icon = Icons.Outlined.Home, - buttonStyle = FabSubItem.ButtonStyle( - color = Color(0xFFE46212), - size = 64.dp - ) - ), - FabSubItem( - onClick = { println("handle mail") }, - icon = Icons.Outlined.MailOutline, - buttonStyle = FabSubItem.ButtonStyle( - color = Color(0xFF3DBFE2), - size = 64.dp - ) - ), - FabSubItem( - onClick = { println("handle place") }, - icon = Icons.Outlined.Place, - buttonStyle = FabSubItem.ButtonStyle( - color = Color(0xFF4AA651), - size = 64.dp +import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig.Orientation +import com.developerstring.jetco.ui.components.button.fab.model.MorphFabItem +import com.developerstring.jetco.ui.components.button.fab.model.StackDirection +import com.developerstring.jetco.ui.components.button.fab.model.StackFabItem +import com.developerstring.jetco.ui.components.button.fab.transition.FabButtonTransition +import com.developerstring.jetco.ui.components.button.fab.transition.FabItemTransition + +private val Orange = Color(0xFFE46212) +private val Teal = Color(0xFF3DBFE2) +private val Green = Color(0xFF4AA651) +private val Red = Color(0xFFDE3B3D) +private val Purple = Color(0xFF7C4DFF) +private val Pink = Color(0xFFE91E8C) +private val Navy = Color(0xFF1A237E) +private val Gold = Color(0xFFFFC107) + +private enum class FabVariant(val label: String) { + RADIAL_CLASSIC("Radial"), + RADIAL_FULL_ARC("Radial 2"), + RADIAL_EXPLOSION("Explosive"), + STACK_VERTICAL("Stack 1"), + STACK_MIXED("Stack 2"), + STACK_HORIZONTAL("H-Stack"), + MORPH_GRID("Morph"), + MORPH_DETAIL("M-Detail") +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun FloatingActionButtonPreview() { + var selected by remember { mutableStateOf(FabVariant.RADIAL_CLASSIC) } + + Box(modifier = Modifier.fillMaxSize()) { + AnimatedContent( + targetState = selected, + transitionSpec = { fadeIn() togetherWith fadeOut() }, + label = "fab_switch", + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 24.dp, bottom = 180.dp) + ) { variant -> + when (variant) { + FabVariant.RADIAL_CLASSIC -> RadialClassicFab() + FabVariant.RADIAL_FULL_ARC -> RadialFullArcFab() + FabVariant.RADIAL_EXPLOSION -> RadialExplosionFab() + FabVariant.STACK_VERTICAL -> StackVerticalFab() + FabVariant.STACK_MIXED -> StackMixedFab() + FabVariant.STACK_HORIZONTAL -> StackHorizontalFab() + FabVariant.MORPH_GRID -> MorphGridFab() + FabVariant.MORPH_DETAIL -> MorphDetailedFab() + } + } + + FlowRow( + maxItemsInEachRow = 4, + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .background(Color.White) + .navigationBarsPadding() + .padding(horizontal = 12.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally) + ) { + FabVariant.entries.forEach { variant -> + FilterChip( + selected = selected == variant, + onClick = { selected = variant }, + label = { + Text( + text = variant.label, + fontSize = 11.sp, + fontWeight = if (selected == variant) FontWeight.Bold else FontWeight.Normal + ) + }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = Orange, + selectedLabelColor = Color.White + ) + ) + } + } + } +} + +@Composable +private fun MorphDetailedFab() { + var expanded by remember { mutableStateOf(false) } + + MorphFloatingActionButton( + expanded = expanded, + onClick = { expanded = !expanded }, + items = emptyList(), + config = FabMainConfig( + buttonStyle = FabMainConfig.ButtonStyle(color = Purple, size = 72.dp), + itemArrangement = FabMainConfig.ItemArrangement( + morph = Orientation.Morph( + columns = 1, + width = 260.dp, + cardShape = RoundedCornerShape(28.dp) + ) + ) + ), + card = { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(64.dp) + .background( + Purple.copy(alpha = 0.1f), RoundedCornerShape(16.dp) + ).clickable(onClick = { expanded = !expanded }), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Outlined.AccountBox, + null, + modifier = Modifier.size(32.dp), + tint = Purple + ) + } + Text( + "User Profile", + Modifier.padding(top = 12.dp), + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + Text( + "System Administrator", + color = Color.Gray, + fontSize = 12.sp + ) + + Row( + Modifier + .padding(top = 20.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + listOf(Icons.Outlined.Edit, Icons.Outlined.Share, Icons.Outlined.Delete).forEach { icon -> + Box( + Modifier.size(44.dp) + .background(Color(0xFFF0F0F0), RoundedCornerShape(10.dp)), + contentAlignment = Alignment.Center + ) { + Icon( + icon, + null, + tint = Color.DarkGray, + modifier = Modifier.size(20.dp) + ) + } + } + } + } + }, + modifier = Modifier.offset(x = if (expanded) 20.dp else 0.dp) + ) +} + +@Composable +private fun StackHorizontalFab() { + var expanded by remember { mutableStateOf(false) } + + StackFloatingActionButton( + expanded = expanded, + onClick = { expanded = !expanded }, + items = listOf( + FabItem(icon = Icons.Outlined.Share, onClick = { expanded = false }), + FabItem(icon = Icons.Outlined.Favorite, onClick = { expanded = false }), + FabItem(icon = Icons.Outlined.Email, onClick = { expanded = false }) + ), + config = FabMainConfig( + buttonStyle = FabMainConfig.ButtonStyle(color = Navy, size = 64.dp), + itemArrangement = FabMainConfig.ItemArrangement(stack = Orientation.Stack(spacedBy = 12.dp)), + animation = FabMainConfig.Animation( + enterTransition = FabItemTransition.Slide() + FabItemTransition.Fade(), + exitTransition = FabItemTransition.Slide() + FabItemTransition.Fade(), + buttonEnterTransition = FabButtonTransition.Rotate(180f) + FabButtonTransition.Scale(0.7f), + buttonExitTransition = FabButtonTransition.Rotate(0f) + FabButtonTransition.Scale(1f) + ) ) - ), - FabSubItem( - onClick = { println("handle delete") }, - icon = Icons.Outlined.Delete, - buttonStyle = FabSubItem.ButtonStyle( - color = Color(0xFFDE3B3D), - size = 64.dp + ) +} + +@Composable +private fun RadialExplosionFab() { + var expanded by remember { mutableStateOf(false) } + + RadialFloatingActionButton( + expanded = expanded, + onClick = { expanded = !expanded }, + items = (1..6).map { i -> + FabItem( + icon = Icons.Outlined.Notifications, + buttonStyle = FabItem.ButtonStyle( + color = listOf(Orange, Teal, Green, Purple, Pink, Gold)[i - 1], + size = 48.dp + ), + onClick = { expanded = false } + ) + }, + config = FabMainConfig( + buttonStyle = FabMainConfig.ButtonStyle(color = Color.Black, size = 60.dp), + itemArrangement = FabMainConfig.ItemArrangement( + radial = Orientation.Radial(arc = Orientation.Radial.Arc.END, radius = 140.dp) + ), + animation = FabMainConfig.Animation( + enterTransition = FabItemTransition.Scale(0) + FabItemTransition.Rotate(720f) + FabItemTransition.Fade(), + exitTransition = FabItemTransition.Scale(0) + FabItemTransition.Rotate(-720f) + FabItemTransition.Fade(), + buttonEnterTransition = FabButtonTransition.Scale(1.1f) + FabButtonTransition.Rotate(225f) + ) ) ) -) +} @Composable -fun RadialFloatingActionButtonPreview() { +private fun RadialClassicFab() { var expanded by remember { mutableStateOf(false) } RadialFloatingActionButton( expanded = expanded, - items = items, onClick = { expanded = !expanded }, + items = listOf( + FabItem( + onClick = { expanded = false }, + icon = Icons.Outlined.Home, + buttonStyle = FabItem.ButtonStyle(color = Orange, size = 56.dp) + ), + FabItem( + onClick = { expanded = false }, + icon = Icons.Outlined.MailOutline, + buttonStyle = FabItem.ButtonStyle(color = Teal, size = 56.dp) + ), + FabItem( + onClick = { expanded = false }, + icon = Icons.Outlined.Place, + buttonStyle = FabItem.ButtonStyle(color = Green, size = 56.dp) + ), + FabItem( + onClick = { expanded = false }, + icon = Icons.Outlined.Delete, + buttonStyle = FabItem.ButtonStyle(color = Red, size = 56.dp) + ) + ), config = FabMainConfig( + buttonStyle = FabMainConfig.ButtonStyle(color = Orange, size = 72.dp), itemArrangement = FabMainConfig.ItemArrangement( - radius = 144.dp + radial = Orientation.Radial( + arc = Orientation.Radial.Arc.END, + radius = 100.dp + ) ), - buttonStyle = FabMainConfig.ButtonStyle( - color = Color(0xFFE46212), - size = 84.dp + animation = FabMainConfig.Animation( + enterTransition = FabItemTransition.Spring() + FabItemTransition.Fade(), + exitTransition = FabItemTransition.Slide() + FabItemTransition.Fade(), + enterOrder = FabMainConfig.StaggerOrder.FIFO, + exitOrder = FabMainConfig.StaggerOrder.FILO, + buttonEnterTransition = FabButtonTransition.Rotate(45f) + FabButtonTransition.Scale(0.85f), + buttonExitTransition = FabButtonTransition.Rotate(0f) + FabButtonTransition.Scale(1f) ) ) ) } @Composable -fun StackFloatingActionButtonPreview() { +private fun RadialFullArcFab() { var expanded by remember { mutableStateOf(false) } - StackFloatingActionButton( + RadialFloatingActionButton( expanded = expanded, - items = items, onClick = { expanded = !expanded }, + items = listOf( + FabItem( + onClick = { expanded = false }, + icon = Icons.Outlined.Edit, + buttonStyle = FabItem.ButtonStyle(color = Purple, size = 52.dp) + ), + FabItem( + onClick = { expanded = false }, + icon = Icons.Outlined.Share, + buttonStyle = FabItem.ButtonStyle(color = Pink, size = 52.dp) + ), + FabItem( + onClick = { expanded = false }, + icon = Icons.Outlined.Favorite, + buttonStyle = FabItem.ButtonStyle(color = Red, size = 52.dp) + ), + FabItem( + onClick = { expanded = false }, + icon = Icons.Outlined.AccountBox, + buttonStyle = FabItem.ButtonStyle(color = Teal, size = 52.dp) + ), + FabItem( + onClick = { expanded = false }, + icon = Icons.Outlined.Delete, + buttonStyle = FabItem.ButtonStyle(color = Gold, size = 52.dp) + ) + ), config = FabMainConfig( buttonStyle = FabMainConfig.ButtonStyle( - color = Color(0xFFE46212), - size = 84.dp + color = Navy, + size = 68.dp, + shape = RoundedCornerShape(20.dp) + ), + itemArrangement = FabMainConfig.ItemArrangement( + radial = Orientation.Radial( + arc = Orientation.Radial.Arc.CENTER, + radius = 110.dp + ) + ), + animation = FabMainConfig.Animation( + enterTransition = FabItemTransition.Scale() + FabItemTransition.Fade() + FabItemTransition.Rotate(-360f), + exitTransition = FabItemTransition.Scale() + FabItemTransition.Fade() + FabItemTransition.Rotate(0f), + enterOrder = FabMainConfig.StaggerOrder.ALL, + exitOrder = FabMainConfig.StaggerOrder.ALL, + buttonEnterTransition = FabButtonTransition.Rotate(135f) + FabButtonTransition.Scale(0.9f) + FabButtonTransition.SlideTo(y = (-8).dp), + buttonExitTransition = FabButtonTransition.Rotate(0f) + FabButtonTransition.Scale(1f) + FabButtonTransition.SlideTo(y = 0.dp) ) ) ) } @Composable -fun MorphFloatingActionButtonPreview() { +private fun StackVerticalFab() { var expanded by remember { mutableStateOf(false) } - MorphFloatingActionButton( + StackFloatingActionButton( expanded = expanded, + onClick = { expanded = !expanded }, items = listOf( - FabSubItem( - onClick = { println("handle home") }, - title = "Home", - icon = Icons.Outlined.Home, - buttonStyle = FabSubItem.ButtonStyle( - color = Color(0xFFE7722A), - shape = RoundedCornerShape(12.dp), - size = 100.dp - ), - titleStyle = FabSubItem.TitleStyle( - weight = FontWeight.Light - ) + FabItem( + onClick = { expanded = false }, + icon = Icons.Outlined.AccountBox, + buttonStyle = FabItem.ButtonStyle(color = Teal, size = 52.dp) ), - FabSubItem( - onClick = { println("handle mail") }, - title = "Mail", - icon = Icons.Outlined.MailOutline, - buttonStyle = FabSubItem.ButtonStyle( - color = Color(0xFFE7722A), - shape = RoundedCornerShape(12.dp), - size = 100.dp - ), - titleStyle = FabSubItem.TitleStyle( - weight = FontWeight.Light - ) + FabItem( + onClick = { expanded = false }, + icon = Icons.Outlined.Edit, + buttonStyle = FabItem.ButtonStyle(color = Purple, size = 52.dp) ), - FabSubItem( - onClick = { println("handle place") }, - title = "Place", - icon = Icons.Outlined.Place, - buttonStyle = FabSubItem.ButtonStyle( - color = Color(0xFFE7722A), - shape = RoundedCornerShape(12.dp), - size = 100.dp - ), - titleStyle = FabSubItem.TitleStyle( - weight = FontWeight.Light - ) + FabItem( + onClick = { expanded = false }, + icon = Icons.Outlined.Share, + buttonStyle = FabItem.ButtonStyle(color = Green, size = 52.dp) ), - FabSubItem( - onClick = { println("handle delete") }, - title = "Delete", + FabItem( + onClick = { expanded = false }, icon = Icons.Outlined.Delete, - buttonStyle = FabSubItem.ButtonStyle( - color = Color(0xFFE7722A), - shape = RoundedCornerShape(12.dp), - size = 100.dp - ), - titleStyle = FabSubItem.TitleStyle( - weight = FontWeight.Light - ) + buttonStyle = FabItem.ButtonStyle(color = Red, size = 52.dp) ) ), - onClick = { expanded = !expanded }, - title = { - Text( - text = "Quick Actions", - color = Color.White + config = FabMainConfig( + buttonStyle = FabMainConfig.ButtonStyle(color = Orange, size = 72.dp), + itemArrangement = FabMainConfig.ItemArrangement( + stack = Orientation.Stack(spacedBy = 16.dp) + ), + animation = FabMainConfig.Animation( + enterTransition = FabItemTransition.SlideAndFade() + FabItemTransition.Scale(), + exitTransition = FabItemTransition.Slide() + FabItemTransition.Fade(), + enterOrder = FabMainConfig.StaggerOrder.FIFO, + exitOrder = FabMainConfig.StaggerOrder.FILO, + buttonEnterTransition = FabButtonTransition.Rotate(45f), + buttonExitTransition = FabButtonTransition.Rotate(0f) ) - }, + ) + ) +} + +@Composable +private fun StackMixedFab() { + var expanded by remember { mutableStateOf(false) } + + StackFloatingActionButton( + expanded = expanded, + onClick = { expanded = !expanded }, + items = listOf( + StackFabItem(direction = StackDirection.TOP) { + Box( + modifier = Modifier + .size(56.dp) + .background(Purple, RoundedCornerShape(16.dp)), + contentAlignment = Alignment.Center + ) { + Text("↑1", color = Color.White, fontWeight = FontWeight.Bold) + } + }, + StackFabItem(direction = StackDirection.TOP) { + Box( + modifier = Modifier + .size(56.dp) + .background(Pink, RoundedCornerShape(16.dp)), + contentAlignment = Alignment.Center + ) { + Text("↑2", color = Color.White, fontWeight = FontWeight.Bold) + } + }, + StackFabItem(direction = StackDirection.START) { + Box( + modifier = Modifier + .size(56.dp) + .background(Teal, RoundedCornerShape(16.dp)), + contentAlignment = Alignment.Center + ) { + Text("←1", color = Color.White, fontWeight = FontWeight.Bold) + } + }, + StackFabItem(direction = StackDirection.START) { + Box( + modifier = Modifier + .size(56.dp) + .background(Green, RoundedCornerShape(16.dp)), + contentAlignment = Alignment.Center + ) { + Text("←2", color = Color.White, fontWeight = FontWeight.Bold) + } + } + ), config = FabMainConfig( buttonStyle = FabMainConfig.ButtonStyle( - color = Color(0xFFE46212), - size = 84.dp + color = Navy, + size = 68.dp, + shape = RoundedCornerShape(20.dp) + ), + itemArrangement = FabMainConfig.ItemArrangement( + stack = Orientation.Stack(spacedBy = 14.dp) + ), + animation = FabMainConfig.Animation( + enterTransition = FabItemTransition.Spring() + FabItemTransition.Scale() + FabItemTransition.Fade(), + exitTransition = FabItemTransition.Slide() + FabItemTransition.Fade(), + enterOrder = FabMainConfig.StaggerOrder.FIFO, + exitOrder = FabMainConfig.StaggerOrder.FILO, + buttonEnterTransition = FabButtonTransition.Rotate(90f) + FabButtonTransition.Scale(0.8f), + buttonExitTransition = FabButtonTransition.Rotate(0f) + FabButtonTransition.Scale(1f) ) ) ) -} \ No newline at end of file +} + +@Composable +private fun MorphGridFab() { + var expanded by remember { mutableStateOf(false) } + + MorphFloatingActionButton( + expanded = expanded, + onClick = { expanded = !expanded }, + items = listOf( + MorphFabItem { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), + modifier = Modifier + .size(96.dp) + .background(Orange, RoundedCornerShape(16.dp)) + ) { + Text("🏠", fontSize = 24.sp) + Text("Home", color = Color.White, fontSize = 11.sp) + } + }, + MorphFabItem { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), + modifier = Modifier + .size(96.dp) + .background(Teal, RoundedCornerShape(16.dp)) + ) { + Text("📷", fontSize = 24.sp) + Text("Camera", color = Color.White, fontSize = 11.sp) + } + }, + MorphFabItem { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), + modifier = Modifier + .size(96.dp) + .background(Purple, RoundedCornerShape(16.dp)) + ) { + Text("✏️", fontSize = 24.sp) + Text("Edit", color = Color.White, fontSize = 11.sp) + } + }, + MorphFabItem { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), + modifier = Modifier + .size(96.dp) + .background(Green, RoundedCornerShape(16.dp)) + ) { + Text("🗺️", fontSize = 24.sp) + Text("Maps", color = Color.White, fontSize = 11.sp) + } + } + ), + config = FabMainConfig( + buttonStyle = FabMainConfig.ButtonStyle(color = Orange, size = 72.dp), + itemArrangement = FabMainConfig.ItemArrangement( + morph = Orientation.Morph( + columns = 2, + spacedBy = 12.dp, + width = 240.dp, + cardShape = RoundedCornerShape(24.dp) + ) + ), + animation = FabMainConfig.Animation( + enterTransition = FabItemTransition.Scale() + FabItemTransition.Fade(), + enterOrder = FabMainConfig.StaggerOrder.FIFO + ) + ) + ) +} diff --git a/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/MainActivity.kt b/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/MainActivity.kt index 824c975..23b232d 100644 --- a/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/MainActivity.kt +++ b/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/MainActivity.kt @@ -1,12 +1,9 @@ package com.developerstring.jetco_library -import android.annotation.SuppressLint import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.material3.FabPosition -import androidx.compose.material3.Scaffold import androidx.compose.ui.graphics.Color import com.developerstring.jetco_library.ui.theme.JetCoLibraryTheme @@ -15,18 +12,12 @@ val LightBlue = Color(0xFFB5DAFF) val LightestPink = Color(0xFFF7F1FF) class MainActivity : ComponentActivity() { - @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { JetCoLibraryTheme { - Scaffold( - floatingActionButton = { - MorphFloatingActionButtonPreview() - }, - floatingActionButtonPosition = FabPosition.End - ) {} + FloatingActionButtonPreview() } } } From aa387213bd3721e48583c9b83b0230ab9e80340d Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Sun, 29 Mar 2026 15:32:13 -0300 Subject: [PATCH 17/19] refactor(fab): remove text support from default items --- .../button/fab/base/DefaultFabItem.kt | 22 ++-------------- .../ui/components/button/fab/model/FabItem.kt | 26 +------------------ 2 files changed, 3 insertions(+), 45 deletions(-) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFabItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFabItem.kt index c1042f7..1052a4f 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFabItem.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/base/DefaultFabItem.kt @@ -8,16 +8,13 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.developerstring.jetco.ui.components.button.fab.model.FabItem @Composable @@ -42,24 +39,9 @@ internal fun DefaultFabItem( item.icon?.let { icon -> Icon( imageVector = icon, - contentDescription = item.title, + contentDescription = null, tint = Color.White, - modifier = Modifier.size( - if (item.title != null) item.buttonStyle.size * 0.4f - else item.buttonStyle.size * 0.55f - ) - ) - } - - item.title?.let { title -> - Text( - text = title, - color = item.titleStyle.color, - fontSize = item.titleStyle.size.value.sp, - fontWeight = item.titleStyle.weight, - maxLines = item.titleStyle.maxLines, - overflow = TextOverflow.Ellipsis, - style = item.titleStyle.style + modifier = Modifier.size(item.buttonStyle.size * 0.55f) ) } } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt index f0551d8..c75cdf1 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt @@ -5,8 +5,6 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -21,18 +19,14 @@ import androidx.compose.ui.unit.dp * which side of the main FAB it spreads toward. * * @param onClick Action invoked when the sub-item is clicked. - * @param title Optional label displayed below the icon. * @param icon Optional icon displayed inside the sub-item button. * @param buttonStyle Visual style of the sub-item button. See [ButtonStyle]. - * @param titleStyle Visual style of the title label. See [TitleStyle]. */ @Stable data class FabItem( val onClick: () -> Unit, - val title: String? = null, val icon: ImageVector? = null, - val buttonStyle: ButtonStyle = ButtonStyle(), - val titleStyle: TitleStyle = TitleStyle() + val buttonStyle: ButtonStyle = ButtonStyle() ) { /** @@ -48,22 +42,4 @@ data class FabItem( val shape: Shape = CircleShape, val size: Dp = 52.dp ) - - /** - * Visual style configuration for the sub-item title label. - * - * @param color Text color of the title. Default is [Color.White]. - * @param size Font size of the title in Dp. Default is 12.dp. - * @param weight Font weight of the title. Default is [FontWeight.Light]. - * @param maxLines Maximum number of lines before the text is ellipsized. Default is 1. - * @param style Base [TextStyle] applied to the title. Default is [TextStyle.Default]. - */ - @Stable - data class TitleStyle( - val color: Color = Color.White, - val size: Dp = 12.dp, - val weight: FontWeight = FontWeight.Light, - val maxLines: Int = 1, - val style: TextStyle = TextStyle.Default - ) } From 05aa45e75579fd57cb298c23addd48c8938c9d8a Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Sun, 29 Mar 2026 21:48:29 -0300 Subject: [PATCH 18/19] feat(fab): add modern interaction to Stack variant --- .../FloatingActionButtonPreview.kt | 300 +++++++++++++----- .../button/fab/StackFloatingActionButton.kt | 16 + .../button/fab/model/FabMainConfig.kt | 3 +- .../button/fab/model/StackExpandOffset.kt | 9 + 4 files changed, 251 insertions(+), 77 deletions(-) create mode 100644 jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackExpandOffset.kt diff --git a/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt b/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt index 809afbb..3896eb5 100644 --- a/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt +++ b/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt @@ -1,6 +1,9 @@ package com.developerstring.jetco_library import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.spring import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith @@ -14,12 +17,20 @@ import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Call +import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.outlined.AccountBox import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Edit @@ -27,13 +38,15 @@ import androidx.compose.material.icons.outlined.Email import androidx.compose.material.icons.outlined.Favorite import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.MailOutline -import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material.icons.outlined.Place import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -53,6 +66,7 @@ import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig.Orientation import com.developerstring.jetco.ui.components.button.fab.model.MorphFabItem import com.developerstring.jetco.ui.components.button.fab.model.StackDirection +import com.developerstring.jetco.ui.components.button.fab.model.StackExpandOffset import com.developerstring.jetco.ui.components.button.fab.model.StackFabItem import com.developerstring.jetco.ui.components.button.fab.transition.FabButtonTransition import com.developerstring.jetco.ui.components.button.fab.transition.FabItemTransition @@ -69,10 +83,10 @@ private val Gold = Color(0xFFFFC107) private enum class FabVariant(val label: String) { RADIAL_CLASSIC("Radial"), RADIAL_FULL_ARC("Radial 2"), - RADIAL_EXPLOSION("Explosive"), STACK_VERTICAL("Stack 1"), STACK_MIXED("Stack 2"), STACK_HORIZONTAL("H-Stack"), + STACK_PUSH("Push"), MORPH_GRID("Morph"), MORPH_DETAIL("M-Detail") } @@ -80,55 +94,126 @@ private enum class FabVariant(val label: String) { @OptIn(ExperimentalLayoutApi::class) @Composable fun FloatingActionButtonPreview() { - var selected by remember { mutableStateOf(FabVariant.RADIAL_CLASSIC) } + var selected by remember { mutableStateOf(FabVariant.STACK_PUSH) } + var expandOffset by remember { mutableStateOf(StackExpandOffset()) } - Box(modifier = Modifier.fillMaxSize()) { - AnimatedContent( - targetState = selected, - transitionSpec = { fadeIn() togetherWith fadeOut() }, - label = "fab_switch", - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(end = 24.dp, bottom = 180.dp) - ) { variant -> - when (variant) { - FabVariant.RADIAL_CLASSIC -> RadialClassicFab() - FabVariant.RADIAL_FULL_ARC -> RadialFullArcFab() - FabVariant.RADIAL_EXPLOSION -> RadialExplosionFab() - FabVariant.STACK_VERTICAL -> StackVerticalFab() - FabVariant.STACK_MIXED -> StackMixedFab() - FabVariant.STACK_HORIZONTAL -> StackHorizontalFab() - FabVariant.MORPH_GRID -> MorphGridFab() - FabVariant.MORPH_DETAIL -> MorphDetailedFab() + val screenOffsetY by animateDpAsState( + targetValue = -expandOffset.offsetY, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow + ), + label = "screenPushY" + ) + val screenOffsetX by animateDpAsState( + targetValue = -expandOffset.offsetX, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow + ), + label = "screenPushX" + ) + + Scaffold( + floatingActionButton = { + AnimatedContent( + targetState = selected, + transitionSpec = { fadeIn() togetherWith fadeOut() }, + label = "fab_switch" + ) { variant -> + when (variant) { + FabVariant.RADIAL_CLASSIC -> RadialClassicFab() + FabVariant.RADIAL_FULL_ARC -> RadialFullArcFab() + FabVariant.STACK_VERTICAL -> StackVerticalFab() + FabVariant.STACK_MIXED -> StackMixedFab() + FabVariant.STACK_HORIZONTAL -> StackHorizontalFab() + FabVariant.STACK_PUSH -> StackPushFab(onExpandChange = { expandOffset = it }) + FabVariant.MORPH_GRID -> MorphGridFab() + FabVariant.MORPH_DETAIL -> MorphDetailedFab() + } } } - - FlowRow( - maxItemsInEachRow = 4, - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .background(Color.White) - .navigationBarsPadding() - .padding(horizontal = 12.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally) + ) { paddingValues -> + Column(modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .offset(y = screenOffsetY, x = screenOffsetX) ) { - FabVariant.entries.forEach { variant -> - FilterChip( - selected = selected == variant, - onClick = { selected = variant }, - label = { - Text( - text = variant.label, - fontSize = 11.sp, - fontWeight = if (selected == variant) FontWeight.Bold else FontWeight.Normal + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items((1..13).toList()) { i -> + Box( + modifier = Modifier + .fillMaxWidth() + .height(if (i == 0) 72.dp else 56.dp) + .background( + color = if (i % 3 == 0) Color(0xFFF0F0F0) + else if (i % 3 == 1) Color(0xFFE8E8E8) + else Color(0xFFDDDDDD), + shape = RoundedCornerShape(16.dp) + ) + ) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Box( + modifier = Modifier + .size(36.dp) + .background(Color(0xFFCCCCCC), CircleShape) + ) + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Box( + modifier = Modifier + .width(120.dp) + .height(10.dp) + .background(Color(0xFFBBBBBB), RoundedCornerShape(4.dp)) + ) + Box( + modifier = Modifier + .width(80.dp) + .height(8.dp) + .background(Color(0xFFCCCCCC), RoundedCornerShape(4.dp)) + ) + } + } + } + } + } + + FlowRow( + maxItemsInEachRow = 4, + modifier = Modifier + .fillMaxWidth() + .background(Color.White) + .navigationBarsPadding() + .padding(horizontal = 12.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally) + ) { + FabVariant.entries.forEach { variant -> + FilterChip( + selected = selected == variant, + onClick = { selected = variant }, + label = { + Text( + text = variant.label, + fontSize = 11.sp, + fontWeight = if (selected == variant) FontWeight.Bold else FontWeight.Normal + ) + }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = Orange, + selectedLabelColor = Color.White ) - }, - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = Orange, - selectedLabelColor = Color.White ) - ) + } } } } @@ -236,37 +321,6 @@ private fun StackHorizontalFab() { ) } -@Composable -private fun RadialExplosionFab() { - var expanded by remember { mutableStateOf(false) } - - RadialFloatingActionButton( - expanded = expanded, - onClick = { expanded = !expanded }, - items = (1..6).map { i -> - FabItem( - icon = Icons.Outlined.Notifications, - buttonStyle = FabItem.ButtonStyle( - color = listOf(Orange, Teal, Green, Purple, Pink, Gold)[i - 1], - size = 48.dp - ), - onClick = { expanded = false } - ) - }, - config = FabMainConfig( - buttonStyle = FabMainConfig.ButtonStyle(color = Color.Black, size = 60.dp), - itemArrangement = FabMainConfig.ItemArrangement( - radial = Orientation.Radial(arc = Orientation.Radial.Arc.END, radius = 140.dp) - ), - animation = FabMainConfig.Animation( - enterTransition = FabItemTransition.Scale(0) + FabItemTransition.Rotate(720f) + FabItemTransition.Fade(), - exitTransition = FabItemTransition.Scale(0) + FabItemTransition.Rotate(-720f) + FabItemTransition.Fade(), - buttonEnterTransition = FabButtonTransition.Scale(1.1f) + FabButtonTransition.Rotate(225f) - ) - ) - ) -} - @Composable private fun RadialClassicFab() { var expanded by remember { mutableStateOf(false) } @@ -490,6 +544,100 @@ private fun StackMixedFab() { ) } +@Composable +private fun StackPushFab(onExpandChange: (StackExpandOffset) -> Unit) { + var expanded by remember { mutableStateOf(false) } + + StackFloatingActionButton( + expanded = expanded, + onClick = { expanded = !expanded }, + onExpandChange = onExpandChange, + items = listOf( + StackFabItem(direction = StackDirection.TOP) { + Box( + modifier = Modifier + .size(56.dp) + .background(Purple, RoundedCornerShape(16.dp)), + contentAlignment = Alignment.Center + ) { + Text("↑1", color = Color.White, fontWeight = FontWeight.Bold) + } + }, + StackFabItem(direction = StackDirection.TOP) { + Box( + modifier = Modifier + .size(56.dp) + .background(Purple, RoundedCornerShape(16.dp)), + contentAlignment = Alignment.Center + ) { + Text("↑2", color = Color.White, fontWeight = FontWeight.Bold) + } + }, + StackFabItem(direction = StackDirection.TOP) { + Box( + modifier = Modifier + .size(56.dp) + .background(Purple, RoundedCornerShape(16.dp)), + contentAlignment = Alignment.Center + ) { + Text("↑3", color = Color.White, fontWeight = FontWeight.Bold) + } + }, + StackFabItem(direction = StackDirection.START) { + var text by remember { mutableStateOf("") } + + Box( + modifier = Modifier.width(330.dp) + ) { + TextField( + value = text, + onValueChange = { text = it }, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 56.dp), + placeholder = { + Text(text = "Hinted search text", color = Color.Gray) + }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + tint = Color.Black + ) + }, + shape = RoundedCornerShape(28.dp), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color(0xFFF3F3F9), + unfocusedContainerColor = Color(0xFFF3F3F9), + disabledContainerColor = Color(0xFFF3F3F9), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + singleLine = true + ) + } + } + ), + config = FabMainConfig( + buttonStyle = FabMainConfig.ButtonStyle( + color = Orange, + size = 64.dp + ), + itemArrangement = FabMainConfig.ItemArrangement( + stack = Orientation.Stack(spacedBy = 14.dp) + ), + animation = FabMainConfig.Animation( + enterTransition = FabItemTransition.Spring() + FabItemTransition.Scale() + FabItemTransition.Fade(), + exitTransition = FabItemTransition.Slide() + FabItemTransition.Fade(), + enterOrder = FabMainConfig.StaggerOrder.FIFO, + exitOrder = FabMainConfig.StaggerOrder.FILO, + buttonEnterTransition = FabButtonTransition.Rotate(45f) + FabButtonTransition.Scale(0.9f), + buttonExitTransition = FabButtonTransition.Rotate(0f) + FabButtonTransition.Scale(1f) + ) + ) + ) +} + @Composable private fun MorphGridFab() { var expanded by remember { mutableStateOf(false) } diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt index 4a1c028..7edda37 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt @@ -23,6 +23,7 @@ import com.developerstring.jetco.ui.components.button.fab.base.DefaultFloatingAc import com.developerstring.jetco.ui.components.button.fab.model.FabItem import com.developerstring.jetco.ui.components.button.fab.model.FabMainConfig import com.developerstring.jetco.ui.components.button.fab.model.StackDirection +import com.developerstring.jetco.ui.components.button.fab.model.StackExpandOffset import com.developerstring.jetco.ui.components.button.fab.model.StackFabItem import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay @@ -35,6 +36,7 @@ fun StackFloatingActionButton( modifier: Modifier = Modifier, onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), + onExpandChange: (StackExpandOffset) -> Unit = {}, content: (@Composable () -> Unit) = { DefaultFloatingActionButton(onClick = onClick, config = config) } @@ -51,6 +53,7 @@ fun StackFloatingActionButton( modifier = modifier, onClick = onClick, config = config, + onExpandChange = onExpandChange, content = content ) } @@ -63,6 +66,7 @@ fun StackFloatingActionButton( modifier: Modifier = Modifier, onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), + onExpandChange: (StackExpandOffset) -> Unit = {}, content: (@Composable () -> Unit) = { DefaultFloatingActionButton(onClick = onClick, config = config) } @@ -73,6 +77,7 @@ fun StackFloatingActionButton( modifier = modifier, onClick = onClick, config = config, + onExpandChange = onExpandChange, content = content ) } @@ -84,6 +89,7 @@ internal fun StackFloatingActionButtonBase( modifier: Modifier = Modifier, onClick: () -> Unit = {}, config: FabMainConfig = FabMainConfig(), + onExpandChange: (StackExpandOffset) -> Unit = {}, content: (@Composable () -> Unit) = { DefaultFloatingActionButton(onClick = onClick, config = config) } @@ -92,6 +98,7 @@ internal fun StackFloatingActionButtonBase( var fabWidthDp by remember { mutableStateOf(config.buttonStyle.size) } var fabHeightDp by remember { mutableStateOf(config.buttonStyle.size) } val spacedBy = config.itemArrangement.stack.spacedBy + val spacingPadding = config.itemArrangement.stack.spacingPadding val itemWidths = remember(items.size) { mutableStateListOf().also { list -> @@ -245,6 +252,15 @@ internal fun StackFloatingActionButtonBase( val btnTransition = if (expanded) config.animation.buttonEnterTransition else config.animation.buttonExitTransition + if (expanded) { + onExpandChange(StackExpandOffset( + offsetY = fabHeightDp + spacingPadding, + offsetX = fabWidthDp + spacingPadding + )) + } else { + onExpandChange(StackExpandOffset()) + } + coroutineScope { launch { fabOffsetX.animateOrSnap(btnTransition.offset?.offsetX, btnTransition.offset?.spec) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt index 5a59eef..56bee4b 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt @@ -49,7 +49,8 @@ data class FabMainConfig( } data class Stack( - val spacedBy: Dp = 40.dp + val spacedBy: Dp = 40.dp, + val spacingPadding: Dp = 24.dp ) : Orientation /** diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackExpandOffset.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackExpandOffset.kt new file mode 100644 index 0000000..d68c571 --- /dev/null +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackExpandOffset.kt @@ -0,0 +1,9 @@ +package com.developerstring.jetco.ui.components.button.fab.model + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +data class StackExpandOffset( + val offsetY: Dp = 0.dp, + val offsetX: Dp = 0.dp +) From 3ae6caead8ca2a113f91ad7571168f489ad58cfd Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Mon, 30 Mar 2026 10:38:40 -0300 Subject: [PATCH 19/19] docs: update FAB component documentation and examples --- .../FloatingActionButtonPreview.kt | 68 +++++++++-------- .../button/fab/MorphFloatingActionButton.kt | 49 ++++++------ .../button/fab/RadialFloatingActionButton.kt | 47 ++++++------ .../button/fab/StackFloatingActionButton.kt | 33 ++++++++ .../ui/components/button/fab/model/FabItem.kt | 27 ++++--- .../button/fab/model/FabMainConfig.kt | 75 +++++++++++-------- .../button/fab/model/MorphFabItem.kt | 13 +++- .../button/fab/model/RadialFabItem.kt | 13 +++- .../button/fab/model/StackFabItem.kt | 10 +++ .../fab/transition/FabButtonTransition.kt | 32 +++++++- .../fab/transition/FabItemTransition.kt | 19 ++++- 11 files changed, 266 insertions(+), 120 deletions(-) diff --git a/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt b/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt index 3896eb5..3929dea 100644 --- a/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt +++ b/jetco-android/JetCoLibrary/app/src/main/java/com/developerstring/jetco_library/FloatingActionButtonPreview.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -55,6 +56,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -115,6 +117,34 @@ fun FloatingActionButtonPreview() { ) Scaffold( + topBar = { + FlowRow( + maxItemsInEachRow = 4, + modifier = Modifier + .statusBarsPadding() + .fillMaxWidth() + .background(Color.White), + horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally) + ) { + FabVariant.entries.forEach { variant -> + FilterChip( + selected = selected == variant, + onClick = { selected = variant }, + label = { + Text( + text = variant.label, + fontSize = 11.sp, + fontWeight = if (selected == variant) FontWeight.Bold else FontWeight.Normal + ) + }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = Orange, + selectedLabelColor = Color.White + ) + ) + } + } + }, floatingActionButton = { AnimatedContent( targetState = selected, @@ -145,7 +175,7 @@ fun FloatingActionButtonPreview() { .padding(horizontal = 16.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - items((1..13).toList()) { i -> + items((1..12).toList()) { i -> Box( modifier = Modifier .fillMaxWidth() @@ -187,34 +217,6 @@ fun FloatingActionButtonPreview() { } } } - - FlowRow( - maxItemsInEachRow = 4, - modifier = Modifier - .fillMaxWidth() - .background(Color.White) - .navigationBarsPadding() - .padding(horizontal = 12.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally) - ) { - FabVariant.entries.forEach { variant -> - FilterChip( - selected = selected == variant, - onClick = { selected = variant }, - label = { - Text( - text = variant.label, - fontSize = 11.sp, - fontWeight = if (selected == variant) FontWeight.Bold else FontWeight.Normal - ) - }, - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = Orange, - selectedLabelColor = Color.White - ) - ) - } - } } } } @@ -547,10 +549,16 @@ private fun StackMixedFab() { @Composable private fun StackPushFab(onExpandChange: (StackExpandOffset) -> Unit) { var expanded by remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current StackFloatingActionButton( expanded = expanded, - onClick = { expanded = !expanded }, + onClick = { + expanded = !expanded + if (!expanded) { + focusManager.clearFocus() + } + }, onExpandChange = onExpandChange, items = listOf( StackFabItem(direction = StackDirection.TOP) { diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt index f00ea45..9688d96 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/MorphFloatingActionButton.kt @@ -27,29 +27,22 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch /** - * A Floating Action Button that morphs into an expanded card grid when activated. + * A floating action button that morphs into a card containing a grid of actions. * - * When collapsed, it displays a standard circular FAB. When expanded, it animates into - * a rounded card containing a title header with a close button, and a configurable grid - * of sub-action items. Sub-items fade in one by one with a staggered entrance animation. + * This component provides a circular FAB that, when expanded, smoothly transitions + * into a rectangular card layout. The card contains a grid of items arranged + * in a [FlowRow], with customizable header space and item arrangement. * - * ## Example Usage: - * ```kotlin - * MorphFloatingActionButton( - * expanded = isExpanded, - * items = listOf( - * FabSubItem( - * onClick = { } - * ) - * ) - * ) - * ``` + * @param expanded Whether the FAB is currently morphed into a card. + * @param items List of [FabItem] to be displayed in the card grid. + * @param modifier Modifier applied to the root container. + * @param onClick Callback triggered when the FAB is clicked or the card's close action is invoked. + * @param config Comprehensive configuration for styling and layout. See [FabMainConfig]. + * @param content Optional custom composable for the collapsed FAB button. + * @param card Optional custom composable for the expanded card shell. Use [MorphCardScope.MorphItems] to place the grid. * - * @param expanded Whether the FAB is currently expanded into card form. - * @param items List of [FabItem] sub-actions to display in the card grid. - * @param modifier Modifier applied to the root [Box] container. - * @param onClick Click handler for both the main FAB button and the card close button. - * @param config Visual and layout configuration. See [FabMainConfig]. + * @see FabMainConfig for detailed arrangement and animation options. + * @see MorphCardScope for customizing the expanded card layout. */ @OptIn(ExperimentalLayoutApi::class) @Composable @@ -83,6 +76,20 @@ fun MorphFloatingActionButton( ) } +/** + * A floating action button that morphs into a card with custom [MorphFabItem]s. + * + * Use this overload when you need to provide fully custom composables for each item + * in the grid instead of the standard icon/button pair. + * + * @param expanded Whether the FAB is currently morphed into a card. + * @param items List of [MorphFabItem] containing custom composables. + * @param modifier Modifier applied to the root container. + * @param onClick Callback triggered when the FAB is clicked. + * @param config Configuration for styling and layout. + * @param content Optional custom composable for the collapsed FAB button. + * @param card Optional custom composable for the expanded card shell. + */ @JvmName("MorphFloatingActionButtonCustom") @Composable fun MorphFloatingActionButton( @@ -142,7 +149,7 @@ internal fun MorphFloatingActionButtonBase( LaunchedEffect(index) { val transition = config.animation.enterTransition - val stepMs = 300 / (items.size - 1) + val stepMs = 300 / (items.size - 1).coerceAtLeast(1) val staggerDelay = config.animation.enterOrder.delayFor( index = index, total = items.size - 1, diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt index 2df6d16..28e898c 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/RadialFloatingActionButton.kt @@ -24,29 +24,21 @@ import kotlin.math.cos import kotlin.math.sin /** - * A Floating Action Button that expands sub-items radially in an arc around the main button. + * A floating action button that expands items in a radial (arc) pattern. * - * When expanded, sub-items fan out in a configurable arc using spring physics with a staggered - * delay, creating a natural and lively feel. When collapsed, items snap back with a clean tween. - * The arc direction and radius are fully configurable via [FabMainConfig.ItemArrangement]. + * This component provides a main FAB that, when expanded, reveals a set of items + * arranged along a configurable arc. It supports both simple [FabItem]s (icons with actions) + * and custom [RadialFabItem]s for full composable control. * - * ## Example Usage: - * ```kotlin - * RadialFloatingActionButton( - * expanded = isExpanded, - * items = listOf( - * FabSubItem( - * onClick = { } - * ) - * ) - * ) - * ``` + * @param expanded Whether the FAB is currently showing its items. + * @param items List of [FabItem] to be displayed as buttons. + * @param modifier Modifier applied to the root container of the FAB. + * @param onClick Callback triggered when the main FAB button is clicked. + * @param config Configuration for styling and animations. See [FabMainConfig]. + * @param content Optional custom composable for the main FAB button. Defaults to [DefaultFloatingActionButton]. * - * @param expanded Whether the FAB is currently expanded, showing sub-items. - * @param items List of [FabItem] sub-actions to display when expanded. - * @param modifier Modifier applied to the root [Box] container. - * @param onClick Click handler for the main FAB button. - * @param config Visual and layout configuration. See [FabMainConfig]. + * @see FabMainConfig for detailed arrangement and animation options. + * @see FabItem for item data model. */ @Composable fun RadialFloatingActionButton( @@ -75,6 +67,19 @@ fun RadialFloatingActionButton( ) } +/** + * A floating action button that expands custom [RadialFabItem]s in a radial (arc) pattern. + * + * Use this overload when you need to provide fully custom composables for each item + * instead of the standard icon/button pair. + * + * @param expanded Whether the FAB is currently showing its items. + * @param items List of [RadialFabItem] containing custom composables. + * @param modifier Modifier applied to the root container of the FAB. + * @param onClick Callback triggered when the main FAB button is clicked. + * @param config Configuration for styling and animations. + * @param content Optional custom composable for the main FAB button. + */ @JvmName("RadialFloatingActionButtonCustom") @Composable fun RadialFloatingActionButton( @@ -137,7 +142,7 @@ internal fun RadialFloatingActionButtonBase( val offsetY = remember { Animatable(0.dp, Dp.VectorConverter) } LaunchedEffect(expanded) { - val stepMs = 300 / (items.size - 1) + val stepMs = 300 / (items.size - 1).coerceAtLeast(1) val order = if (expanded) config.animation.enterOrder else config.animation.exitOrder val transition = if (expanded) config.animation.enterTransition else config.animation.exitTransition val staggerDelay = order.delayFor(index = index, total = (items.size - 1), stepMs = stepMs) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt index 7edda37..16f3e3b 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/StackFloatingActionButton.kt @@ -29,6 +29,26 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +/** + * A floating action button that expands items in a linear stack (Vertical or Horizontal). + * + * This component allows for expanding items in multiple directions (TOP, START, END) + * simultaneously. It also provides an [onExpandChange] callback that reports the total + * displacement of the main button, which can be used to "push" other UI elements + * (like screen content) out of the way. + * + * @param expanded Whether the FAB is currently showing its items. + * @param items List of [FabItem] to be displayed as buttons. Defaults to [StackDirection.TOP]. + * @param modifier Modifier applied to the root container. + * @param onClick Callback triggered when the main FAB button is clicked. + * @param config Configuration for styling and animations. See [FabMainConfig]. + * @param onExpandChange Callback that provides [StackExpandOffset] + * containing the total width/height of expanded main FAB + [FabMainConfig.Orientation.Stack.spacingPadding]. + * @param content Optional custom composable for the main FAB button. + * + * @see StackExpandOffset for handling screen content displacement. + * @see StackFabItem for custom composable items with specific directions. + */ @Composable fun StackFloatingActionButton( expanded: Boolean, @@ -58,6 +78,19 @@ fun StackFloatingActionButton( ) } +/** + * A floating action button that expands custom [StackFabItem]s in a linear stack. + * + * Use this overload to specify different directions for each item or to use custom composables. + * + * @param expanded Whether the FAB is currently showing its items. + * @param items List of [StackFabItem] with direction and custom content. + * @param modifier Modifier applied to the root container. + * @param onClick Callback triggered when the main FAB button is clicked. + * @param config Configuration for styling and animations. + * @param onExpandChange Callback that provides [StackExpandOffset] data. + * @param content Optional custom composable for the main FAB button. + */ @JvmName("StackFloatingActionButtonCustom") @Composable fun StackFloatingActionButton( diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt index c75cdf1..1be2852 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabItem.kt @@ -7,20 +7,19 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.developerstring.jetco.ui.components.button.fab.StackFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.MorphFloatingActionButton +import com.developerstring.jetco.ui.components.button.fab.RadialFloatingActionButton /** - * Data model representing a single sub-action item displayed by a FAB variant. + * Data model representing a standard item in a Floating Action Button (FAB) menu. * - * Each [FabItem] carries an action callback, an optional icon, an optional title, - * and independent style configurations for both the button and the title label. - * The title is only rendered when the FAB variant explicitly enables it. + * This is the default item type used by [RadialFloatingActionButton], [StackFloatingActionButton], + * and [MorphFloatingActionButton]. It consists of an icon, a click action, and visual styling. * - * For [StackFloatingActionButton], each item can also carry a [direction] that controls - * which side of the main FAB it spreads toward. - * - * @param onClick Action invoked when the sub-item is clicked. - * @param icon Optional icon displayed inside the sub-item button. - * @param buttonStyle Visual style of the sub-item button. See [ButtonStyle]. + * @property onClick Callback triggered when this specific item is clicked. + * @property icon to be displayed inside the item button. + * @property buttonStyle Visual configuration for this item, including color, shape, and size. */ @Stable data class FabItem( @@ -30,11 +29,11 @@ data class FabItem( ) { /** - * Visual style configuration for the sub-item button. + * Visual style configuration for an individual [FabItem]. * - * @param color Background color of the sub-item button. Default is Material blue. - * @param shape Shape of the sub-item button. Default is [CircleShape]. - * @param size Diameter of the sub-item button. Default is 52.dp. + * @property color Background color of the item button. + * @property shape Shape of the item button. Default is [CircleShape]. + * @property size Diameter/Height of the item button. Default is 52.dp. */ @Stable data class ButtonStyle( diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt index 56bee4b..daf0b78 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/FabMainConfig.kt @@ -1,7 +1,5 @@ package com.developerstring.jetco.ui.components.button.fab.model -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -14,11 +12,15 @@ import com.developerstring.jetco.ui.components.button.fab.transition.FabButtonTr import com.developerstring.jetco.ui.components.button.fab.transition.FabItemTransition /** - * Main configuration class for all Floating Action Button variants in JetCo. + * Main configuration class for the Floating Action Button (FAB) family. * - * @param buttonStyle Visual appearance of the main FAB button. See [ButtonStyle]. - * @param itemArrangement Layout and orientation of sub-items. See [ItemArrangement]. - * @param animation Animation timing and spec used across all transitions. See [Animation]. + * This class centralizes all visual and behavioral settings, including button styling, + * item arrangement patterns (Radial, Stack, Morph), and animation specifications + * for both the main button and its items. + * + * @property buttonStyle Visual styling for the main FAB (color, shape, size, etc.). + * @property itemArrangement Configuration for how items are positioned when expanded. + * @property animation Animation specs for entry/exit transitions and stagger orders. */ @Stable data class FabMainConfig( @@ -28,12 +30,15 @@ data class FabMainConfig( ) { /** - * Sealed interface representing the available layout orientations for FAB sub-items. - * - * Use [Radial] for arc-based spreading, [Stack] for linear stacking, - * and [Morph] for the card expansion layout. + * Defines the layout orientation and specific parameters for FAB expansion. */ sealed interface Orientation { + /** + * Radial orientation that expands items in an arc. + * + * @property arc The direction and span of the arc (END, START, CENTER). + * @property radius The distance from the main FAB to the sub-items. + */ data class Radial( val arc: Arc = Arc.END, val radius: Dp = 80.dp @@ -48,6 +53,12 @@ data class FabMainConfig( } } + /** + * Stack orientation that expands items linearly. + * + * @property spacedBy Gap between consecutive items in the stack. + * @property spacingPadding Extra padding used to report total expansion size in [StackExpandOffset]. + */ data class Stack( val spacedBy: Dp = 40.dp, val spacingPadding: Dp = 24.dp @@ -56,11 +67,11 @@ data class FabMainConfig( /** * Morph orientation that expands the main FAB into a card grid. * - * @param columns Number of sub-item columns in the card grid. Default is 2. - * @param spacedBy Gap between sub-items inside the grid. Default is 12.dp. - * @param headerSpace Space between the card header and the item grid. Default is 20.dp. - * @param width Total width of the expanded card. Default is 250.dp. - * @param cardShape Shape of the expanded card. Default is [RoundedCornerShape] with 24.dp. + * @property columns Number of item columns in the card grid. + * @property spacedBy Gap between items inside the grid. + * @property headerSpace Space between the card header and the item grid. + * @property width Total width of the expanded card. + * @property cardShape Shape of the expanded card. */ data class Morph( val columns: Int = 2, @@ -72,11 +83,14 @@ data class FabMainConfig( } /** - * Animation configuration shared across all FAB variants. + * Animation configuration for the FAB family. * - * @param durationMillis Duration of each animation in milliseconds. Default is 300. - * @param easing Easing curve applied to tween-based animations. Default is [FastOutSlowInEasing]. - * @param animationSpec Full [AnimationSpec] used for float animations such as alpha and rotation. + * @property enterOrder The order in which items appear (FIFO, FILO, ALL). + * @property exitOrder The order in which items disappear. + * @property enterTransition Combined transitions for sub-item entry. + * @property exitTransition Combined transitions for sub-item exit. + * @property buttonEnterTransition Transitions applied to the main FAB on expansion. + * @property buttonExitTransition Transitions applied to the main FAB on collapse. */ @Stable open class Animation( @@ -88,10 +102,16 @@ data class FabMainConfig( val buttonExitTransition: FabButtonTransition = FabButtonTransition.Rotate(0f) ) + /** + * Controls the sequence of staggered animations for sub-items. + */ @Stable enum class StaggerOrder { + /** First In, First Out: items animate in the order they appear in the list. */ FIFO, + /** First In, Last Out: items animate in reverse order. */ FILO, + /** All items animate simultaneously. */ ALL; internal fun delayFor(index: Int, total: Int, stepMs: Int): Long = when (this) { @@ -104,28 +124,23 @@ data class FabMainConfig( /** * Visual style configuration for the main FAB button. * - * @param color Background color of the main FAB. Default is Material blue. - * @param shape Shape of the main FAB button. Default is [CircleShape]. - * @param horizontalSpace Horizontal gap between icon and text when both are present. Default is 12.dp. - * @param size Diameter (and height) of the main FAB. Width expands via [defaultMinSize] when text is added. Default is 72.dp. - * @param iconRotation Target rotation angle of the icon when the FAB is expanded. Default is 45f. - * @param padding Internal padding applied inside the FAB button row. Default is no padding. + * @property color Background color of the main FAB. + * @property shape Shape of the main FAB button. + * @property size Diameter/Height of the main FAB. + * @property iconRotation Target rotation of the icon when expanded (legacy support). + * @property padding Internal padding for the button content. */ @Stable data class ButtonStyle( val color: Color = Color(0xFF1976D2), val shape: Shape = CircleShape, - val horizontalSpace: Dp = 12.dp, val size: Dp = 72.dp, val iconRotation: Float = 45f, val padding: PaddingValues = PaddingValues() ) /** - * Layout and orientation configuration for FAB sub-items. - * - * @param stack Stack direction orientation. Default is [Orientation.Stack.TOP]. - * @param morph Morph card configuration. Default is [Orientation.Morph]. + * Container for all arrangement-specific settings. */ @Stable data class ItemArrangement( diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/MorphFabItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/MorphFabItem.kt index 1318dbc..ba75ca7 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/MorphFabItem.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/MorphFabItem.kt @@ -1,7 +1,18 @@ package com.developerstring.jetco.ui.components.button.fab.model import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import com.developerstring.jetco.ui.components.button.fab.MorphFloatingActionButton +/** + * A wrapper for a custom composable to be used as an item in [MorphFloatingActionButton]. + * + * Use this class when you want to provide a fully custom UI for an item inside + * the morphed card grid instead of the default [FabItem]. + * + * @property content The composable content to be rendered for this item in the grid. + */ +@Stable class MorphFabItem( val content: @Composable () -> Unit -) \ No newline at end of file +) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RadialFabItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RadialFabItem.kt index 22dc934..c3abb33 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RadialFabItem.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/RadialFabItem.kt @@ -1,7 +1,18 @@ package com.developerstring.jetco.ui.components.button.fab.model import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import com.developerstring.jetco.ui.components.button.fab.RadialFloatingActionButton +/** + * A wrapper for a custom composable to be used as an item in [RadialFloatingActionButton]. + * + * Use this class when you want to provide a fully custom UI for a radial menu item + * instead of the default icon-based [FabItem]. + * + * @property content The composable content to be rendered for this radial item. + */ +@Stable class RadialFabItem( val content: @Composable () -> Unit -) \ No newline at end of file +) diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackFabItem.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackFabItem.kt index 3698c67..3fcd057 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackFabItem.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/model/StackFabItem.kt @@ -2,7 +2,17 @@ package com.developerstring.jetco.ui.components.button.fab.model import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import com.developerstring.jetco.ui.components.button.fab.StackFloatingActionButton +/** + * A wrapper for a custom composable with a specific direction to be used as an item in [StackFloatingActionButton]. + * + * Use this class when you need to specify a different expansion direction for each item + * or when you want to provide a fully custom UI instead of the default [FabItem]. + * + * @property direction The direction in which this item will expand from the main FAB (TOP, START, END). + * @property content The composable content to be rendered for this item. + */ @Stable class StackFabItem( val direction: StackDirection = StackDirection.TOP, diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabButtonTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabButtonTransition.kt index 6765168..16a2bd1 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabButtonTransition.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabButtonTransition.kt @@ -8,12 +8,30 @@ import androidx.compose.animation.core.tween import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +/** + * Describes how the main FAB button animates when the component expands or collapses. + * + * Transitions can be combined using the `+` operator. + * + * ## Example: + * ```kotlin + * val buttonTransition = FabButtonTransition.Rotate(45f) + FabButtonTransition.Scale(0.8f) + * ``` + * + * @property offset Translation animation for the main button. + * @property scale Scale animation for the main button. + * @property rotation Rotation animation for the main button. + * @property spring Optional global spring override for all properties. + */ class FabButtonTransition internal constructor( val offset: OffsetTransition? = null, val scale: ScaleTransition? = null, val rotation: RotateTransition? = null, val spring: SpringTransition? = null ) { + /** + * Combines two button transitions. Properties in [other] take precedence. + */ operator fun plus(other: FabButtonTransition): FabButtonTransition = FabButtonTransition( offset = other.offset ?: this.offset, scale = other.scale ?: this.scale, @@ -23,6 +41,9 @@ class FabButtonTransition internal constructor( companion object { + /** + * Animates the main button position to a specific (x, y) offset. + */ fun SlideTo( x: Dp = 0.dp, y: Dp = 0.dp, @@ -36,6 +57,9 @@ class FabButtonTransition internal constructor( ) ) + /** + * Scales the main button to a specific target value. + */ fun Scale( scale: Float, durationMillis: Int = 300, @@ -47,6 +71,9 @@ class FabButtonTransition internal constructor( ) ) + /** + * Rotates the main button to a specific target angle. + */ fun Rotate( rotation: Float, durationMillis: Int = 300, @@ -58,6 +85,9 @@ class FabButtonTransition internal constructor( ) ) + /** + * Overrides the animation spec with physics-based spring behavior. + */ fun Spring( dampingRatio: Float = Spring.DampingRatioMediumBouncy, stiffness: Float = Spring.StiffnessMedium @@ -67,4 +97,4 @@ class FabButtonTransition internal constructor( ) ) } -} \ No newline at end of file +} diff --git a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabItemTransition.kt b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabItemTransition.kt index e7e1f78..4764517 100644 --- a/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabItemTransition.kt +++ b/jetco-android/JetCoLibrary/jetco/ui/src/main/java/com/developerstring/jetco/ui/components/button/fab/transition/FabItemTransition.kt @@ -8,6 +8,23 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.ui.unit.Dp +/** + * Describes how an item (FAB) animates when it enters or exits the screen. + * + * Transitions can be combined using the `+` operator. + * Each factory function returns a new [FabItemTransition] that only defines the specific + * animation spec for its property. + * + * ## Example: + * ```kotlin + * val myItemTransition = FabItemTransition.Spring() + FabItemTransition.Fade() + * ``` + * + * @property offsetSpec Animation for the item's movement (displacement). + * @property alphaSpec Animation for the item's transparency. + * @property scaleSpec Animation for the item's size (X and Y). + * @property rotate Configuration for the item's rotation. + */ class FabItemTransition( val offsetSpec: AnimationSpec? = null, val alphaSpec: AnimationSpec? = null, @@ -69,4 +86,4 @@ class FabItemTransition( ) ) } -} \ No newline at end of file +}