From da33cc9aa4c74b4af95120bc133b5f98b0c36e4a Mon Sep 17 00:00:00 2001 From: Vinnih-1 Date: Thu, 19 Mar 2026 22:38:01 -0300 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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() + } } }