Skip to content
Open
Original file line number Diff line number Diff line change
@@ -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
)
)
)
}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
) {}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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.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

/**
* 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 config Visual and layout configuration. See [FabMainConfig].
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MorphFloatingActionButton(
expanded: Boolean,
items: List<FabSubItem>,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
title: (@Composable () -> Unit)? = null,
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)

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)
) {
// 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) {
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 {
Box(contentAlignment = Alignment.Center) {
content()
}
}
}
}
Loading