Kotlin Multiplatform – Float Action Button with Menu Buttons
Create Scaffold with the following FAB settings:
Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, topBar = { TopAppBar( windowInsets = AppBarDefaults.topAppBarWindowInsets, title = { Text(text = "app bar title") }, navigationIcon = { IconButton(onClick = { onClick() }) { Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = "Back" ) } } ) }, floatingActionButton = { MultiFloatingButton( multiFloatingState = multiFloatingState, onMultiFabStateChange = { multiFloatingState = it }, items = items, hostState = snackbarHostState ) } )
Create FAB Menu items and additional variables with state:
var multiFloatingState by remember { mutableStateOf(MultiFloatingState.Colapsed) } val items = listOf( MinFabItem( icon = Icons.Filled.Add, label = "Attachment", identifier = Identifier.Attachment.name ), MinFabItem( icon = Icons.Filled.Home, label = "Home", identifier = Identifier.Home.name ), MinFabItem( icon = Icons.Filled.Cloud, label = "Cloud", identifier = Identifier.Cloud.name ) ) val snackbarHostState = remember { SnackbarHostState() }
Create Composable functions for FAB:
enum class MultiFloatingState { Expanded, Colapsed } enum class Identifier { Attachment, Home, Cloud } @Composable fun MultiFloatingButton( multiFloatingState: MultiFloatingState, onMultiFabStateChange: (MultiFloatingState) -> Unit, items: List<MinFabItem>, hostState: SnackbarHostState ) { val transition = updateTransition(targetState = multiFloatingState, label = "transition") val rotate by transition.animateFloat(label = "rotate") { if (it == MultiFloatingState.Expanded) 315f else 0f; } val fabScale by transition.animateFloat(label = "FabScale") { if (it == MultiFloatingState.Expanded) 56f else 0f } val alpha by transition.animateFloat( label = "alhpa", transitionSpec = { tween(durationMillis = 50) }) { if (it == MultiFloatingState.Expanded) 1f else 0f } val textShadow by transition.animateDp( label = "textShadow", transitionSpec = { tween(durationMillis = 50) }) { if (it == MultiFloatingState.Expanded) 2.dp else 0.dp } val scope = rememberCoroutineScope() Column( horizontalAlignment = Alignment.End ) { if (transition.currentState == MultiFloatingState.Expanded) { items.forEach { MinFab( item = it, onItemMinFabItemClick = { minFabItem -> when (minFabItem.identifier) { Identifier.Attachment.name -> { scope.launch { val result = hostState .showSnackbar( message = "Attachment", actionLabel = "Action", // Defaults to SnackbarDuration.Short duration = SnackbarDuration.Indefinite ) when (result) { SnackbarResult.ActionPerformed -> { /* Handle snackbar action performed */ } SnackbarResult.Dismissed -> { /* Handle snackbar dismissed */ } } } } Identifier.Home.name -> { scope.launch { val result = hostState .showSnackbar( message = "Home", actionLabel = "Action", // Defaults to SnackbarDuration.Short duration = SnackbarDuration.Indefinite ) when (result) { SnackbarResult.ActionPerformed -> { /* Handle snackbar action performed */ } SnackbarResult.Dismissed -> { /* Handle snackbar dismissed */ } } } } Identifier.Cloud.name -> { } } }, alpha = alpha, textShadow = textShadow, fabScale = fabScale ) Spacer(modifier = Modifier.size(16.dp)) } } FloatingActionButton(onClick = { onMultiFabStateChange( if (transition.currentState == MultiFloatingState.Expanded) { MultiFloatingState.Colapsed } else { MultiFloatingState.Expanded } ) }) { Icon( imageVector = Icons.Filled.Add, contentDescription = null, modifier = Modifier.rotate(rotate) ) } } } class MinFabItem( val icon: ImageVector, val label: String, val identifier: String ) @Composable fun MinFab( item: MinFabItem, alpha: Float, textShadow: Dp, fabScale: Float, showLabel: Boolean = true, onItemMinFabItemClick: (MinFabItem) -> Unit ) { val buttonColor = MaterialTheme.colors.secondary val shadow = Color.Black.copy(.5f) val painter = rememberVectorPainter(image = item.icon) Row() { if (showLabel) { Text( item.label, fontSize = 12.sp, fontWeight = FontWeight.Bold, modifier = Modifier.alpha( animateFloatAsState(targetValue = alpha, animationSpec = tween(50)).value ) .shadow(textShadow) .background(MaterialTheme.colors.background) .padding(start = 6.dp, end = 6.dp, top = 4.dp) ) Spacer(modifier = Modifier.size(16.dp)) } Canvas( modifier = Modifier.size(42.dp).clickable( interactionSource = MutableInteractionSource(), onClick = { onItemMinFabItemClick.invoke(item) }, indication = rememberRipple( bounded = false, radius = 20.dp, color = MaterialTheme.colors.onSurface ) ) ) { drawCircle( color = shadow, radius = fabScale, center = Offset( center.x + 2f, center.y + 2f ) ) drawCircle( color = buttonColor, radius = fabScale ) translate(top = 25f, left = 25f) { with(painter) { draw(painter.intrinsicSize, alpha = alpha) } } } } }