How do I animate the visibility of a weighted composable?

468 Views Asked by At

I have this composable here with a history, an input/output, and a button to show/hide the history (I have removed the parameters unnecessary to my concern):

@Composable
fun MainWindow(
    isShowingHistory: Boolean,
    showHistory: () -> Unit,
    hideHistory: () -> Unit,
) {
    Column(modifier = Modifier.fillMaxSize()) {
        History(modifier = Modifier.weight(1f))
        Surface(
            color = contentColor.copy(0.04f),
            modifier = Modifier.fillMaxWidth(),
        ) {
            Column(
                horizontalAlignment = Alignment.End,
                verticalArrangement = Arrangement.Bottom,
            ) {
                if (!isShowingHistory) {
                    Spacer(modifier = Modifier.weight(1f))
                }
                InputResult(isCompact = isShowingHistory)
                if (!isShowingHistory) {
                    Spacer(modifier = Modifier.weight(1f))
                }
                HistoryIconButton(
                    onClick = if (!isShowingHistory) showHistory else hideHistory,
                    modifier = Modifier.padding(16.dp),
                )
            }
        }
    }
}

It looks like this:

If isShowingHistory is false

isShowingHistory is false

If isShowingHistory is true

isShowingHistory is true

The appearance is correct. However, I want to animate the entrance of History. One way I thought I could make that work is by using AnimatedVisibility on the spacers. However, since the spacers use the modifier weight, I cannot do that since weight only works for direct children of the column.

@Composable
fun MainWindow(
    isShowingHistory: Boolean,
    showHistory: () -> Unit,
    hideHistory: () -> Unit,
) {
    Column(modifier = Modifier.fillMaxSize()) {
        History(modifier = Modifier.weight(1f))
        Surface(
            color = contentColor.copy(0.04f),
            modifier = Modifier.fillMaxWidth(),
        ) {
            Column(
                horizontalAlignment = Alignment.End,
                verticalArrangement = Arrangement.Bottom,
            ) {
                AnimatedVisibility(visible = !isShowingHistory) {
                    Spacer(modifier = Modifier.weight(1f))
                }
                InputResult(isCompact = isShowingHistory)
                AnimatedVisibility(visible = !isShowingHistory) {
                    Spacer(modifier = Modifier.weight(1f))
                }
                HistoryIconButton(
                    onClick = if (!isShowingHistory) showHistory else hideHistory,
                    modifier = Modifier.padding(16.dp),
                )
            }
        }
    }
}

If isShowingHistory is false while using AnimatedVisibility

Using AnimatedVisibility

I have also tried moving the weight modifier to the AnimatedVisibility like this:

@Composable
fun MainWindow(
    isShowingHistory: Boolean,
    showHistory: () -> Unit,
    hideHistory: () -> Unit,
) {
    Column(modifier = Modifier.fillMaxSize()) {
        History(modifier = Modifier.weight(1f))
        Surface(
            color = contentColor.copy(0.04f),
            modifier = Modifier.fillMaxWidth(),
        ) {
            Column(
                horizontalAlignment = Alignment.End,
                verticalArrangement = Arrangement.Bottom,
            ) {
                AnimatedVisibility(
                    visible = !isShowingHistory,
                    modifier = Modifier.weight(1f),
                ) {}
                InputResult(isCompact = isShowingHistory)
                AnimatedVisibility(
                    visible = !isShowingHistory,
                    modifier = Modifier.weight(1f),
                ) {}
                HistoryIconButton(
                    onClick = if (!isShowingHistory) showHistory else hideHistory,
                    modifier = Modifier.padding(16.dp),
                )
            }
        }
    }
}

However, this does not animate the visibility of the spacers, even if I explicitly set the enter and exit parameters to expandVertically and shrinkVertically, since AnimatedVisibility animates its contents, not itself.

How do I animate the weighted spacers? Or better yet, how do I properly animate the visibility of the History composable, following the layout shown in the first two screenshots?

1

There are 1 best solutions below

1
On

How about this solution

Short demo video

The main goal is to calc height and set it to History box. Let's say you wanted History takes 70% of the parent's height.


[parent].onGloballyPositioned {
            val rect = it.boundsInParent()
            //Lets' say you want the history takes 70% of the height
            historyHeightPx = (rect.bottom * 0.7F).toInt()
        })
AnimatedVisibility(visible = isShowingHistory) {
    History(modifier = Modifier
                    .height(
                        with(LocalDensity.current) { historyHeightPx.toDp() }
                     )
     )
}

Full code:

            var historyVisibility by remember { mutableStateOf(false) }

            TestLayout(
                isShowingHistory = historyVisibility,
                showHistory = {
                    historyVisibility = true
                }, hideHistory = {
                    historyVisibility = false
                })
@Composable
fun TestLayout(
    isShowingHistory: Boolean,
    showHistory: () -> Unit,
    hideHistory: () -> Unit,
) {

    var historyHeightPx by remember { mutableIntStateOf(0) }

    Box(modifier = Modifier
        .fillMaxWidth()
        .onGloballyPositioned {
            val rect = it.boundsInParent()
            //Lets' say you want the history takes 70% of the height
            historyHeightPx = (rect.bottom * 0.7F).toInt()
        }) {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Bottom
        ) {

            AnimatedVisibility(visible = isShowingHistory) {
                History(modifier = Modifier
                    .height(with(LocalDensity.current) { historyHeightPx.toDp() })
                )
            }

            InputResult(isCompact = isShowingHistory)
        }

        HistoryIconButton(modifier = Modifier.align(alignment = Alignment.BottomEnd), onClick = {
            if (isShowingHistory) hideHistory() else showHistory()
        })
    }
}

History and InputResult boxes:

@Composable
fun ColumnScope.History(modifier: Modifier) {
    Box(
        modifier = modifier
            .background(color = Color.Gray)
            .fillMaxWidth(),
        contentAlignment = Alignment.BottomEnd
    ) {
        Text(text = "History here")
    }
}

@Composable
fun ColumnScope.InputResult(isCompact: Boolean) {
    Box(
        modifier = Modifier
            .background(color = Color.Blue)
            .fillMaxWidth()
            .weight(1f),
        contentAlignment = Alignment.TopEnd
    ) {
        Text(text = "Inputs here")
    }
}