Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
da33cc9
feat(fab): add radial fab component for android
Vinnih-1 Mar 20, 2026
e60f968
feat(fab): add stack fab variant for android
Vinnih-1 Mar 20, 2026
01417c7
feat(fab): add padding to ButtonStyle for better customization
Vinnih-1 Mar 20, 2026
ce74260
fix(fab): adjust UI alignment and rotation animation
Vinnih-1 Mar 20, 2026
063a4dd
feat(fab): add morph fab variant for android
Vinnih-1 Mar 21, 2026
410b188
fix(fab): add fab height tracking to fix subitem overlap in TOP orien…
Vinnih-1 Mar 21, 2026
60bb97d
docs(fab): add documentation to all FAB components and models
Vinnih-1 Mar 21, 2026
bf427e5
chore(fab): add preview composable for FAB components
Vinnih-1 Mar 21, 2026
e8b7786
chore: rename BaseFloatingActionButton to DefaultFloatingActionButton
Vinnih-1 Mar 25, 2026
bb34326
feat(fab): add content slot for custom UI
Vinnih-1 Mar 25, 2026
660e729
feat(fab): refine sub item animation behavior
Vinnih-1 Mar 26, 2026
19b3264
feat(fab): allow custom morph card implementation
Vinnih-1 Mar 27, 2026
9c9c1c8
feat(fab): add animation for main FAB interactions
Vinnih-1 Mar 28, 2026
cf1cb6a
feat(fab): add support for custom composable items
Vinnih-1 Mar 28, 2026
bf0b24c
feat(fab): modularize transitions and refine stack behavior
Vinnih-1 Mar 29, 2026
1dcdb9d
chore(fab): expand preview with new component usage examples
Vinnih-1 Mar 29, 2026
aa38721
refactor(fab): remove text support from default items
Vinnih-1 Mar 29, 2026
05aa45e
feat(fab): add modern interaction to Stack variant
Vinnih-1 Mar 30, 2026
3ae6cae
docs: update FAB component documentation and examples
Vinnih-1 Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
JetCoLibraryTheme {
LineGraphScreen()
FloatingActionButtonPreview()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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

internal suspend fun <T, V : AnimationVector> Animatable<T, V>.animateOrSnap(
targetValue: T?,
spec: AnimationSpec<T>?,
predicate: () -> Boolean = { true }
) {
if (targetValue == null) return

if (spec != null) {
animateTo(targetValue, spec)
} else if (predicate.invoke()) {
snapTo(targetValue)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package com.developerstring.jetco.ui.components.button.fab

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.size
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 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.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
import kotlinx.coroutines.launch

/**
* A floating action button that morphs into a card containing a grid of actions.
*
* 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.
*
* @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.
*
* @see FabMainConfig for detailed arrangement and animation options.
* @see MorphCardScope for customizing the expanded card layout.
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MorphFloatingActionButton(
expanded: Boolean,
items: List<FabItem>,
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)
}
) {
val items = items.map { item ->
MorphFabItem {
DefaultFabItem(item = item, onClick = { item.onClick() })
}
}

MorphFloatingActionButtonBase(
expanded = expanded,
items = items,
modifier = modifier,
onClick = onClick,
config = config,
content = content,
card = card
)
}

/**
* 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(
expanded: Boolean,
items: List<MorphFabItem>,
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,
items = items,
modifier = modifier,
onClick = onClick,
config = config,
content = content,
card = card
)
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
internal fun MorphFloatingActionButtonBase(
expanded: Boolean,
items: List<MorphFabItem>,
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)
}
) {
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, morphItem ->
// 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
val stepMs = 300 / (items.size - 1).coerceAtLeast(1)
val staggerDelay = config.animation.enterOrder.delayFor(
index = index,
total = items.size - 1,
stepMs = stepMs
)

delay(staggerDelay)

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)
}
}

Box(
modifier = Modifier.graphicsLayer {
this.alpha = alpha.value
this.scaleX = scale.value
this.scaleY = scale.value
this.rotationZ = rotation.value
}
) {
morphItem.content()
}
}
}
}
val scope = MorphCardScope(
itemsContent = itemsContent
)

AnimatedContent(
targetState = expanded,
label = "MorphFloatingActionButton",
modifier = modifier
) {
if (it) {
scope.card()
} else {
Box {
content()
}
}
}
}
Loading