Floating Action Button with Material Animations

Author: Johan Reitan

Floating Action Button with animations that are implemented as per the Material Design spec.

FAB Demo

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.FloatingActionButtonDefaults
import androidx.compose.material.FloatingActionButtonElevation
import androidx.compose.material.MaterialTheme
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.material.FloatingActionButton as MaterialFloatingActionButton

/**
 * FAB with show/hide animations according to the Material spec
 */
@Composable
fun FloatingActionButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    visible: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
    backgroundColor: Color = MaterialTheme.colors.secondary,
    contentColor: Color = contentColorFor(backgroundColor),
    elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
    content: @Composable () -> Unit,
) {
    AnimatedVisibility(
        modifier = modifier,
        visible = visible,
        enter = fadeIn(
            animationSpec = tween(
                durationMillis = 15,
                delayMillis = 30,
                easing = LinearEasing,
            ),
        ),
        exit = fadeOut(
            animationSpec = tween(
                durationMillis = 15,
                delayMillis = 150,
                easing = LinearEasing,
            )
        ),
    ) {
        val fabScale by transition.animateFloat(
            transitionSpec = {
                tween(
                    durationMillis = when (targetState) {
                        EnterExitState.PreEnter,
                        EnterExitState.Visible,
                        -> 330
                        EnterExitState.PostExit -> 135
                    },
                    delayMillis = 0,
                    easing = LinearOutSlowInEasing,
                )
            },
            label = "FAB scale"
        ) {
            when (it) {
                EnterExitState.PreEnter,
                EnterExitState.PostExit,
                -> 0f
                EnterExitState.Visible -> 1f
            }
        }
        MaterialFloatingActionButton(
            modifier = Modifier.graphicsLayer {
                scaleX = fabScale
                scaleY = fabScale
            },
            onClick = onClick,
            interactionSource = interactionSource,
            shape = shape,
            backgroundColor = backgroundColor,
            contentColor = contentColor,
            elevation = elevation,
        ) {
            val contentScale by transition.animateFloat(
                transitionSpec = {
                    tween(
                        durationMillis = when (targetState) {
                            EnterExitState.PreEnter,
                            EnterExitState.Visible,
                            -> 240
                            EnterExitState.PostExit -> 135
                        },
                        delayMillis = when (targetState) {
                            EnterExitState.PreEnter,
                            EnterExitState.Visible,
                            -> 90
                            EnterExitState.PostExit -> 0
                        },
                        easing = FastOutLinearInEasing,
                    )
                },
                label = "FAB content scale"
            ) {
                when (it) {
                    EnterExitState.PreEnter,
                    EnterExitState.PostExit,
                    -> 0f
                    EnterExitState.Visible -> 1f
                }
            }
            Box(
                Modifier.graphicsLayer {
                    scaleX = contentScale
                    scaleY = contentScale
                }
            ) {
                content()
            }
        }
    }
}

Have a project you'd like to submit? Fill this form, will ya!

If you like this snippet, you might also like:

Maker OS is an all-in-one productivity system for developers

I built Maker OS to track, manage & organize my life. Now you can do it too!