Toggleable FAB supports animating its container size, corner radius, and color when it is toggled, and should be used in conjunction with a FloatingActionButtonMenu to provide additional choices to the user after clicking the FAB.
Common
@ExperimentalMaterial3ExpressiveApi
@Composable
fun ToggleFloatingActionButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
containerColor: (Float) -> Color = ToggleFloatingActionButtonDefaults.containerColor(),
contentAlignment: Alignment = Alignment.TopEnd,
containerSize: (Float) -> Dp = ToggleFloatingActionButtonDefaults.containerSize(),
containerCornerRadius: (Float) -> Dp =
ToggleFloatingActionButtonDefaults.containerCornerRadius(),
content: @Composable ToggleFloatingActionButtonScope.() -> Unit,
)
Parameters
| checked | whether this Toggleable FAB is checked |
| onCheckedChange | callback to be invoked when this Toggleable FAB is clicked, therefore the change of the state in requested |
| modifier | the Modifier to be applied to this Toggleable FAB |
| containerColor | the color used for the background of this Toggleable FAB, based on the checked progress value from 0-1 |
| contentAlignment | the alignment of this Toggleable FAB when checked |
| containerSize | the size of this Toggleable FAB, based on the checked progress value from 0-1 |
| containerCornerRadius | the corner radius of this Toggleable FAB, based on the checked progress value from 0-1 |
| content | the content of this Toggleable FAB, typically an Icon that switches from an Add to a Close sign at 50% checked progress |
Code Examples
FloatingActionButtonMenuSample
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Preview
@Composable
fun FloatingActionButtonMenuSample() {
val listState = rememberLazyListState()
val fabVisible by remember {
derivedStateOf {
listState.firstVisibleItemIndex == 0 || listState.canScrollForward == false
}
}
val focusRequester = remember { FocusRequester() }
Box {
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
for (index in 0 until 100) {
item {
Text(
text = "List item - $index",
modifier = Modifier.clickable {}.fillMaxWidth().padding(24.dp),
)
}
}
}
val items =
listOf(
Icons.AutoMirrored.Filled.Message to "Reply",
Icons.Filled.People to "Reply all",
Icons.Filled.Contacts to "Forward",
Icons.Filled.Snooze to "Snooze",
Icons.Filled.Archive to "Archive",
Icons.AutoMirrored.Filled.Label to "Label",
)
var fabMenuExpanded by rememberSaveable { mutableStateOf(false) }
BackHandler(fabMenuExpanded) { fabMenuExpanded = false }
FloatingActionButtonMenu(
modifier = Modifier.align(Alignment.BottomEnd),
expanded = fabMenuExpanded,
button = {
// A FAB should have a tooltip associated with it.
TooltipBox(
positionProvider =
TooltipDefaults.rememberTooltipPositionProvider(
if (fabMenuExpanded) {
TooltipAnchorPosition.Start
} else {
TooltipAnchorPosition.Above
}
),
tooltip = { PlainTooltip { Text("Toggle menu") } },
state = rememberTooltipState(),
) {
ToggleFloatingActionButton(
modifier =
Modifier.semantics {
traversalIndex = -1f
stateDescription =
if (fabMenuExpanded) "Expanded" else "Collapsed"
contentDescription = "Toggle menu"
}
.animateFloatingActionButton(
visible = fabVisible || fabMenuExpanded,
alignment = Alignment.BottomEnd,
)
.focusRequester(focusRequester),
checked = fabMenuExpanded,
onCheckedChange = { fabMenuExpanded = !fabMenuExpanded },
) {
val imageVector by remember {
derivedStateOf {
if (checkedProgress > 0.5f) Icons.Filled.Close else Icons.Filled.Add
}
}
Icon(
painter = rememberVectorPainter(imageVector),
contentDescription = null,
modifier = Modifier.animateIcon({ checkedProgress }),
)
}
}
},
) {
items.forEachIndexed { i, item ->
FloatingActionButtonMenuItem(
modifier =
Modifier.semantics {
isTraversalGroup = true
// Add a custom a11y action to allow closing the menu when focusing
// the last menu item, since the close button comes before the first
// menu item in the traversal order.
if (i == items.size - 1) {
customActions =
listOf(
CustomAccessibilityAction(
label = "Close menu",
action = {
fabMenuExpanded = false
true
},
)
)
}
}
.then(
if (i == 0) {
Modifier.onKeyEvent {
// Navigating back from the first item should go back to the
// FAB menu button.
if (
it.type == KeyEventType.KeyDown &&
(it.key == Key.DirectionUp ||
(it.isShiftPressed && it.key == Key.Tab))
) {
focusRequester.requestFocus()
return@onKeyEvent true
}
return@onKeyEvent false
}
} else {
Modifier
}
),
onClick = { fabMenuExpanded = false },
icon = { Icon(item.first, contentDescription = null) },
text = { Text(text = item.second) },
)
}
}
}
}