Would anyone be able to provide some guidance on h...
# compose
a
Would anyone be able to provide some guidance on how to implement an animated expanding/collapsing navigation rail similar to what is shown here (which is from the Navigation drawer section of this page)? The issue that I'm running into is related to how the text in the FAB or list items behaves when the rail is collapsing. As you can see in the animation, the desired effect is to have the text (i.e. "Compose" and "Inbox") fade out as the rail collapses with the result being that only the icons are visible. However, in practice, since the FAB and list items are contained within the rail, the text gets wrapped during the animation as the width of the rail animates to the collapsed size. The only thing I have found to "work" is to set the
wrapContentWidth(unbounded = true)
modifier on the
Text
composable, however, this has the side-effect of never wrapping the text, which I don't necessarily want. I started looking into creating a transition and using
AnimatedContent
but I haven't been having much luck so far.
πŸ‘€ 1
d
Recommend taking a look at
expandHorizontally() + fadeIn()
and
shrinkHorizontally() + fadeOut()
for the text. See: https://developer.android.com/reference/kotlin/androidx/compose/animation/package-summary#expandHorizontally(androidx[…]in.Boolean,kotlin.Function1) I would use AnimatedVisibility for the text instead of AnimatedContent for the two layouts, because there is a shared element between the two layouts -- the orange compose button. We don't currently support shared element yet.
r
@Andrew Hughes Can you share the code snippet of switching navigation rail?
a
@Doris Liu Thanks for the pointers. I looked at
AnimatedVisibility
and I was again able to make it work, but I had to use
.wrapContentWidth(unbounded = true)
on the
Text
composable. @rsktash Here's a simplified example that demonstrates the issue:
Copy code
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun SimpleNavRail(
    expanded: Boolean,
    onExpandClick: () -> Unit,
    onCreateClick: () -> Unit,
) {
    val transition = updateTransition(targetState = expanded, label = "Expanded")

    val width by transition.animateDp(label = "Width") { if (it) 256.dp else 72.dp }
    Surface(modifier = Modifier.width(width)) {
        Column {
            IconButton(
                onClick = onExpandClick,
                modifier = Modifier.align(Alignment.End)
            ) {
                Icon(
                    imageVector = if (expanded) Icons.Default.ArrowBack else Icons.Default.ArrowForward,
                    contentDescription = null, // TODO
                    modifier = Modifier.size(24.dp),
                    tint = MaterialTheme.colors.primary,
                )
            }

            Surface(
                modifier = Modifier
                    .padding(8.dp)
                    .height(56.dp)
                    .clickable(onClick = onCreateClick),
                shape = if (expanded) RoundedCornerShape(33.dp) else CircleShape,
                color = MaterialTheme.colors.primary,
            ) {
                Row(
                    verticalAlignment = Alignment.CenterVertically,
                ) {
                    val iconHorizontalPadding by transition.animateDp(label = "FAB Horizontal Padding") { if (it) 12.dp else 16.dp }
                    Icon(
                        imageVector = Icons.Default.Add,
                        contentDescription = null, // TODO
                        modifier = Modifier
                            .padding(horizontal = iconHorizontalPadding)
                            .size(24.dp)
                    )
                    transition.AnimatedVisibility(
                        visible = { expanded -> expanded },
                        enter = fadeIn(),
                        exit = fadeOut(),
                    ) {
                        Text(
                            text = "Create",
                            style = MaterialTheme.typography.button,
                            modifier = Modifier
                                .padding(end = 20.dp)
                                .wrapContentWidth(unbounded = true)
                        )
                    }
                }
            }
        }
    }
}

@Preview
@Composable
private fun SimpleNavRailExpandedPreview() {
    var expanded by remember { mutableStateOf(true) }
    SimpleNavRail(
        expanded = expanded,
        onExpandClick = { expanded = !expanded },
        onCreateClick = { },
    )
}
If you remove the
.wrapContentWidth(unbounded = true)
on the
Text
composable you'll see that the word "Create" wraps during the transition when the button collapses. It's definitely much less visible in the current iteration than it has been before, but I can't figure out how to do this "properly" without it. And if the button actually needs the text to wrap, I'm not sure how that would work.
On a related note, the animation inspection tool in AS is awesome! However, it doesn't appear to be working on the
AnimatedVisibility
. πŸ˜• I assume that's just because it's a work-in-progress. Since I can't inspect the
AnimatedVisibility
transition, it would helpful to be able to slow down the entire transition. Is there a way to do that? I've only found ways to slow down individual components of the transition by specifying an
animationSpec
.
d
Expand and shrink enter/exit transition achieves resizing via clipping, instead of actually resizing the text, to avoid unintended wrapping. πŸ™‚
AnimatedVisibility inspection is supported in Android studio canary builds, btw. I would recommend giving that a try.
a
Awesome, thanks! I'll check out canary. Oh, gotcha. Okay, I'll look into the expand and shrink enter/exit transitions more. I forgot to say that I did look at them, but found that they added undesired effects on the text transition. What I want is for the text to simply fade in/out and for it to just get clipped instead of wrap. I'll see if I can configured those to do that.
d
You may need to change the
expandFrom
parameter to
Alignment.Start
to achieve what you need
a
I just gave that a try and I think it might be working...but it's hard to tell since I'm not sure how to slow down the transition as a whole. I'm in canary now and unfortunately I'm still not seeing the
AnimatedVisibility
transition in the animation inspection.
d
Did you enable the "interactive and animation preview tools" in the experimental setting from the perferences?
a
You're talking about these right?
d
Uh, sorry. I just checked as well. It's actually not in Bumblebee, but rather the next named version of Android studio. 😐
a
Or is there something else as well?
Oh! Gotcha.
d
No, that would be the right flag
a
Is there a way to slow down the transition as a whole?
d
Not at the moment, unfortunately. There's a bug tracking it: https://issuetracker.google.com/161675988
a
Thanks. ⭐'d. I think it would also be helpful to simply be able to set a duration on a transition as a whole, especially for debugging (e.g.
transition.durationMillis = 5000
). Is that something that's on the roadmap or I should submit a feature request for on the bug tracker?
I was able to slow the transition down by setting the transition and animation specs using longer durations and I discovered that the issue is due to animating the width of the parent
Surface
composable. For the expand case, the issue is that the button is not being laid out as if the parent
Surface
composable were at the target width (256 dp). Instead, the button is constantly being re-laid out as the nav rail expands, causing the text to reflow (despite the
expandHorizontally()
enter transition). Here's a GIF demonstrating what I'm seeing with a long string on the button to make it easier to see.
And here's the corresponding code
d
If I understand you correctly, you want to be able to set the duration on the transition level, and every animation in that transition should then switch to a 5000ms tween?
a
Hmm. I guess when you put it that way, it does make less sense. Since the default animations are not tween animations, but spring animations. So I guess what I'd rather see is a way to simply slow down a particular transition, which is basically the developer option in the issue you mentioned, but it seems like it would be nice to do that to just a particular transition when debugging.... but supporting the developer option probably solves that...
d
I wonder if it'd be useful to support a feature to allow each Transition to be individually configured with a playback speed. πŸ˜„
a
Yes, exactly
d
As for the code, it'd be much easier if you could figure out what the full size of the text. Then you can just set the text size to a fixed size, and do shrink/expand on the text to influence the overall surface size. That'd be having surface wrap the text, and the clipped text would resize surface as it animates the clip on its way in/out. It's the reverse of your current approach of defining surface size and calculate text size based on the animating surface size
a
Here's what it looks like if I don't animate the width of the nav rail (parent
Surface
) as a part of the transition. As you can see the
AnimatedVisibilty
is working perfectly because the width of the parent isn't interfering.
d
Yup. That's what I was suggesting. πŸ™‚
Except you could have the surface wrap content, so that the surface gets a "derived" animation
a
I'm digesting what you're saying πŸ˜…
d
Try giving the text a width of ~180.dp, and remove the width modifier in the outter Surface
a
Okay, that definitely works in terms of the transition and animating everything correctly, but then the expanded nav rail isn't the correct size and would be based on the text size (which will fluctuate with translations).
Oh! But if I add a
Spacer(modifier = Modifier.width(width))
this causes the parent
Surface
to animate to the same width constraints that I was setting directly on the
Surface
.
d
I was doing a simple subtraction of the two different width targets in your animation to get the size of the text. Do you need to support a dynamic width based on the text size?
a
The drawer should always be the same width when expanded, but the width of the button should change based on the length of the text, which could be longer or shorter depending on translation. I'm not sure yet if the text would ever be long enough that it would wrap, so the previously mentioned
wrapContentWidth(unbounded = true)
may have sufficed, but I'm also trying to learn the compose animation APIs, so I was hoping there was a way to do this without relying on that. πŸ˜‰
d
If the drawer should always be the same width, and the icons should be the same width, you should be able to derive the max text size based on the two above.
It's the easiest solution to constraint the text to that width, and wrap as needed. Alternatively, you could create custom modifier for the outer surface. This modifier will always measure children based on the full size, but clip that full size based on the animation value.
a
Right, so basically if I set a max width on the text such that the button (+padding) doesn't exceed 256 dp, and do the same thing with all the other stuff that needs to go in the rail below the button, then the nav rail width should always be correct based on the
Spacer
that is enforcing the width.
Oh? Is there an example of how to write a custom modifier like that?
d