2

I'm trying to create a multi-item floating action button with the following animation:

I created a multi-item floating action button but I could not implement the intended animation.

I have FilterFabMenuButton composable that I show as a menu item :

FilterFabMenuButton

@Composable
fun FilterFabMenuButton(
    item: FilterFabMenuItem,
    onClick: (FilterFabMenuItem) -> Unit,
    modifier: Modifier = Modifier
) {

    FloatingActionButton(
        modifier = modifier,
        onClick = {
            onClick(item)
        },
        backgroundColor = colorResource(
            id = R.color.primary_color
        )
    ) {
        Icon(
            painter = painterResource(item.icon), contentDescription = null, tint = colorResource(
                id = R.color.white
            )
        )
    }
}

I have FilterFabMenuLabel composable which is a label for FilterFabMenuButton:

FilterFabMenuLabel

@Composable
fun FilterFabMenuLabel(
    label: String,
    modifier: Modifier = Modifier
) {

    Surface(
        modifier = modifier,
        shape = RoundedCornerShape(6.dp),
        color = Color.Black.copy(alpha = 0.8f)
    ) {
        Text(
            text = label, color = Color.White,
            modifier = Modifier.padding(horizontal = 20.dp, vertical = 2.dp),
            fontSize = 14.sp,
            maxLines = 1
        )
    }

}

I have FilterFabMenuItem composable which is a Row that contains FilterFabMenuLabel and FilterFabMenuButton composables:

FilterFabMenuItem

@Composable
fun FilterFabMenuItem(
    menuItem: FilterFabMenuItem,
    onMenuItemClick: (FilterFabMenuItem) -> Unit,
    modifier: Modifier = Modifier
) {

    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.spacedBy(10.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {

        //label
        FilterFabMenuLabel(label = menuItem.label)

        //fab
        FilterFabMenuButton(item = menuItem, onClick = onMenuItemClick)

    }

}

I have FilterFabMenu composable which is a Column that shows menu items:

FilterFabMenu

@Composable
fun FilterFabMenu(
    visible: Boolean,
    items: List<FilterFabMenuItem>,
    modifier: Modifier = Modifier
) {

    val enterTransition = remember {
        expandVertically(
            expandFrom = Alignment.Bottom,
            animationSpec = tween(150, easing = FastOutSlowInEasing)
        ) + fadeIn(
            initialAlpha = 0.3f,
            animationSpec = tween(150, easing = FastOutSlowInEasing)
        )
    }

    val exitTransition = remember {
        shrinkVertically(
            shrinkTowards = Alignment.Bottom,
            animationSpec = tween(150, easing = FastOutSlowInEasing)
        ) + fadeOut(
            animationSpec = tween(150, easing = FastOutSlowInEasing)
        )
    }


    AnimatedVisibility(visible = visible, enter = enterTransition, exit = exitTransition) {
        Column(
            modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.End,
            verticalArrangement = Arrangement.spacedBy(16.dp),
        ) {
            items.forEach { menuItem ->
                FilterFabMenuItem(
                    menuItem = menuItem,
                    onMenuItemClick = {}
                )
            }
        }
    }
}

I have FilterFab composable that expands/collapses FilterMenu:

FilterFab

@Composable
fun FilterFab(
    state: FilterFabState,
    rotation:Float,
    onClick: (FilterFabState) -> Unit,
    modifier: Modifier = Modifier
) {
       FloatingActionButton(
           modifier = modifier
               .rotate(rotation),
           elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 0.dp),
           onClick = {
               onClick(
                   if (state == FilterFabState.EXPANDED) {
                       FilterFabState.COLLAPSED
                   } else {
                       FilterFabState.EXPANDED
                   }
               )
           },
           backgroundColor = colorResource(
               R.color.primary_color
           ),
           shape = CircleShape
       ) {
           Icon(
               painter = painterResource(R.drawable.fab_add),
               contentDescription = null,
               tint = Color.White
           )
       }

}

Last but not least, I have a FilterView composable which is a Column that contains FilterFabMenu and FilterFab composables:

FilterView

@SuppressLint("UnusedTransitionTargetStateParameter")
@Composable
fun FilterView(
    items: List<FilterFabMenuItem>,
    modifier: Modifier = Modifier
) {

    var filterFabState by rememberSaveable() {
        mutableStateOf(FilterFabState.COLLAPSED)
    }

    val transitionState = remember {
        MutableTransitionState(filterFabState).apply {
            targetState = FilterFabState.COLLAPSED
        }
    }

    val transition = updateTransition(targetState = transitionState, label = "transition")

    val iconRotationDegree by transition.animateFloat({
        tween(durationMillis = 150, easing = FastOutSlowInEasing)
    }, label = "rotation") {
        if (filterFabState == FilterFabState.EXPANDED) 230f else 0f
    }

    Column(
        modifier = modifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.End,
        verticalArrangement = Arrangement.spacedBy(16.dp,Alignment.Bottom)
    ) {
        FilterFabMenu(items = items, visible = filterFabState == FilterFabState.EXPANDED)
        FilterFab(
            state = filterFabState,
            rotation = iconRotationDegree, onClick = { state ->
                filterFabState = state
            })
    }
}

This produces the following result:

Jarnojr
  • 543
  • 1
  • 7
  • 18

1 Answers1

0

expandVertically in your enterTransition is not the correct approach for this kind of animation. Per documentation, it animates a clip revealing the content of the animated item from top to bottom, or vice versa. You apply this animation to the entire column (so all items at once), resulting in the gif you have shown us.

Instead, you should use a different enter/exit animation type, maybe a custom animation where you work with the item scaling to emulate the "pop in" effect like such:

scaleFactor.animateTo(2f, tween(easing = FastOutSlowInEasing, durationMillis = 50))
scaleFactor.animateTo(1f, tween(easing = FastOutSlowInEasing, durationMillis = 70))

(the scaleFactor is an animatabale of type Animatable<Float, AnimationVector1D> in this instance).

Then you create such an animatable for each of the column items, i.e. your menu items. After that, just run the animations in a for loop for each menu item inside a coroutine scope (since compose animations are suspend by default, they will run in sequence, use async/awaitAll if you want to do it in parallel).

Also, don't forget to put your animatabales in remember {}, then just use the values you are animating like scaleFactor inside modifiers of your column items, and trigger them inside a LaunchedEffect when you click to expand/close the menu.

just_deko
  • 1,024
  • 1
  • 14
  • 29