Im using `AnimatedContent` to transition betwen ac...
# compose
z
Im using
AnimatedContent
to transition betwen active/idle states here, out of which both should be centered vertically. However.. if you look closely at the video, the animation dithers (I think thats the correct term) due to the sizes being different. This seems like such a simple problem to solve, and perhaps it is?
s
Got any code to share? Does the blue card have some sort of entering animation itself too? Because it seems to come from outside its own bounds. Also, could you describe what the optimal visual effect would be in this case instead?
z
Copy code
TransitionContent(
    modifier = modifier.debugBorder(),
    targetState = active,
    effect = // fadeIn + expandIn togetherWith fadeOut + shrinkOut
    alignment = Alignment.Center,
    content = { resting ->
        if (resting) {
            Pill(
                color = Theme.palette.surfaceContainerHighest,
                size = Large,
                onClick = onOpen,
                onLongClick = onStop,
                fill =
                    rememberUpdatedFill(
                        color = if (expiring) Theme.palette.negative else Theme.palette.primaryContainer,
                        fraction = progress,
                    ),
                content = {
                    Alarm(icon, expiring)
                    Spacer(SpaceMedium)
                    label()
                },
            )
        } else {
            IconButton(
                icon = icon,
                onClick = onOpen,
                onLongClick = onStart,
            )
        }
    },
)
Under the hood its just AnimatedContent. I think the blue pill comes from outside of its bounds due to me using clip=false on the animation. It does look much better with it.
As for the optimal visual effect, what you see in the video, minus the dithering ๐Ÿ˜… I can always fine tune those things later on I feel, but I have to get the sizing to work before I can move any further.
s
I think I don't actually know what "dithering" means, which makes it a bit harder for me to understand hehe ๐Ÿ˜…
z
Probably a bad choice of word on my end, but Im referring to how the content shakes when the animation nears its end. I think you can see it quite well in the video, although I may have just looked at it for long enough that I cant see anything else!
I think its the layout struggling to find the vertical center due to the sizes being adjusted, at least thats what it looks like
s
Oh shit you're right, I see it too now. If you do not center align it, how does it look like instead? Is it that it starts from too high up, but without the jumpiness? I suppose here it's even more special since the height difference is quite a small one, so it makes these jumps very often. If it was a bigger difference it'd probably look better.
z
Yes exactly, it starts from top left.. and that looks weird, but other than that - no issues with the sizing. Youre probably right about the small height difference not playing to my advantage here, I dont think Ive ever noticed this behavior before! I mean, if all else fails, I could throw the stuff in a SubcomposeLayout and then use the max dimension between the two. It just seems like such a large thing to do, theres gotta be a better way.. right, right? ๐Ÿ˜… Ive also never used any animations together with SubcomposeLayout.. so no clue whether they will affect how the content is measured. Ive also considered using a custom modifier that just keeps the layout max size between the two.. but that would only work when going from idle -> active (it can start in active too!). Any other ideas? ๐Ÿ›Œ๐Ÿฝ
s
One idea could be a shared elements transition between these two states, where you can also make the clock actually be a shared element here. Another could be that you don't use AnimatedContent, but instead animate the text coming in on the right side, and you also animate the color of the card changing. But everything would change "inline" so to speak. Like the card itself transforming instead of a new similarly looking card coming into the screen. Another would be to take the text you have in the active state, pass it through a TextMeasurer and see what the height of it would be, and make the idle state card take at least that height. This also avoids sub composition and you'll have the right height on the first frame.
z
Thanks for responding so late ๐Ÿ˜ƒ I have a feeling that Id run into trouble when using shared transitions, because the icon in its standalone form is an IconButton (larger in size) and in active just a regular icon. I havent used shared transitions much though, so this might not be an issue? If you wanna put together some code, Id be happy to try it. I can always fake the IconButton to just look like one and have it inside the Pill (I think this is what you mean with your other 2 suggestions?); I did that in the past, just using AnimatedVisibility on the text and changing pill colors depending on state, but as things get added to it.. its really hard to keep it in a functional state. As for TextMeasurer, good idea, but the text is smaller than the IconButton (sorry I dont think I mentioned that its one in my initial post). The what seems to me straightforward solution is to just use a Box and have everything inside it, animating in/out, that way nothing is relative to the other in terms of size. Even SharedTransitions could work with this? Afaik (or remember) I did try it, just cant recall why I moved away from it. Ill give this a shot in a few hours when I can sit down and write some code again!
Same shaking effect with shared transitions sad panda
t
I see in your code itโ€™s two components. Why not make it just a box with rounded corners and animate the background color and the text showing? ๐Ÿ‘€
z
That kinda works @Tgo1014 but from my experiments, animating between just showing the IconButton and the Icon with text causes the shaking issue in the video. If I place all content inside the pill, either I have to exactly recreate an IconButton as a regular icon (this is hard, Ive tried ๐Ÿ˜…) or the pill will be huuge (its like 24.dp ish in size, IconButton is like 48.dp I think).
t
Why not just a Box with a click? Do you need all the extra button behaviour?
z
Im using a lot of stuff from IconButton in the regular idle state icon, so yes I could copy the code and make it work, but at some point the designs will diverge and Ill be in a world of pain by doing so I think. The IconButton in the vid sits next to 2 other IconButtons, so its important that they look & behave the same way.
t
Isnโ€™t something like this that you need?
z
Can you share the code?
t
Copy code
var isShowing by remember { mutableStateOf(false) }
val background by animateColorAsState(
    if (isShowing) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
)
val shape = CircleShape
Row(
    modifier = Modifier
        .clip(shape)
        .clickable { isShowing = !isShowing }
        .background(background, shape = shape)
        .padding(8.dp)
) {
    Icon(Icons.Default.Build, contentDescription = null)
    AnimatedVisibility(isShowing) {
        Text("This is my long text")
    }
}
z
Yes, thats pretty much what im going for. So my stubborness to use IconButton is the main issue ๐Ÿ˜ž
t
Isnโ€™t the entire component clickable?
Icon button for me is when itโ€™s only a icon always
I mean, you can make it looks like the other IconButton and make it work the way you want haha
z
I appreciate you putting together the code for this. I guess my main concern is that it will diverge from the IconButton, mine does quite a lot with colors and other styling. I can always copy that and try to mimic it as closely as possible, but thats kind of why Im here trying to solve this in a better way. In the past I created a custom composable for it, but as time went on and I had to add things to it (badge for example) then it became an absolute mess to maintain (taking badge as an example, the badge container was larger than the pill, and the badge was shown in the incorrect position due to regular icon being so small, at least thats what I think caused it).
๐Ÿ‘๐Ÿฝ 1
Thanks to all the combined ideas in this thread and some AI, I think Ive got something working that matches all my ridiculous requirements ๐Ÿ˜… Im pretty sure this was one of the early prototypes too (minus shared element transition) but ended up tossing it because I incorrectly thought that it didnt work. Basically
SharedTransitionLayout { Row { Pill() IconButton() } }
with icon using
Modifier.sharedElement
so that it moves across. Of course, the shaking issue is sitll present, just less noticeable because the vertical size no longer plays a role. Now just gotta figure out why it still shakes + stop the icon from blinking during the shared element transition (this is the first time Ive really tried using them, so no clue what Im doing).
t
Wow, it looks super cool. In the full speed one I canโ€™t notice any shaking, looks really nice!
s
I think this looks amazing too, a great outcome imo!
z
Thank you guys ๐Ÿซถ๐Ÿพ Managed to "fix" the blinking issue too by using:
Copy code
sharedBounds(state, scope, enter = EnterTransition.None, exit = ExitTransition.None)
I dont know if thats correct, but it seems to work.
s
Perhaps this just makes the bounds "jump" to the final size instead of having it slowly shrink while in animation, making the bounciness go away. I wonder if what you're really looking for here is
Modifier.skipToLookaheadSize()
. I'd give that a try too
z
Where would I place that modifier? Seems like the blinking is caused by Modifier.sharedElement eventually leading into using fadeIn + fadeOut, but Im not sure why that would be the default.. seems odd that a shared element fades, kinda breaks the continuity! But its experimental, and Im a newbie at it, so many loose ends ๐Ÿ˜…
s
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[โ€ฆ]les/SharedTransitionSamples.kt;l=86-100?q=skipToLookaheadSize I think just somewhere after you've put your
shared*
modifier. In your case perhaps just chain it directly after on the card/iconbutton itself? I am not sure either, I'd play around with it. Shared element shouldn't be using a fade by default, shared bounds does however since the content is not meant to be the exact same visually, so it's a sane default. Are you sure there is a fade in the default shared element overload you're using?
z
Ill play around with it! As for the fade, what you mention sounds logical and theres no fade in just Modifier.sharedElement, but I dont know why the icon blinks then.
s
Yeah that shouldn't be happening. Perhaps in that path inside your AnimatedContent you got a 1 frame delay of showing the icon somehow. So on the first frame it's not present in composition.
z
I am animating the icons color, but that should start with a color and not delay its visibility I feel like?
s
Does not sound like it should no. I'd try stripping things away until it works probably
z
Thats what Ill do. It might be that the standalone icon has a foreground that matches the background of the toolbar, whereas the other one matches the pill background. Looks similar, but theres probably a slight difference that causes the icon to kind of fade? Time will tell, I hope Ill have a chance to test it soon ๐Ÿ˜„
Issue was that I had fade in TransitionVisibility, so ironically I was fading the icon myself.. so just removing the fade solves the issue. But it leaves me wondering why I need to do this at all, Im using TransitionVisibility (AnimatedVisibility) in order to animate the visibility of pill / just icon, but ultimately I dont want any animation running, I just need AnimatedVisibilityScope for the shared transition ๐Ÿคท๐Ÿฝ
Btw, skipToLookaheadSize did not seem to do anything. I tried to put it on pill, icon, and the entire row.
Even though its technically not correct, using
Copy code
context(SharedTransitionScope)
@Composable
private fun Modifier.sharedIcon(
    scope: AnimatedVisibilityScope,
    state: SharedContentState = rememberSharedContentState("icon"),
) = sharedBounds(
    sharedContentState = state,
    animatedVisibilityScope = scope,
    enter = EnterTransition.None,
    exit = ExitTransition.None,
)
Gives me a much better effect, because I do want other composables to fade, just not specifically the shared element (icon). Ive tried to put Modifier.animateEnterExit in various places, but I think thats problematic due to me wanting to animate the pill itself (which would then fade the icon). If anyone knows a better way etc, please teach me!
s
Did you put separate
sharedX
modifiers on each thing individually? To me it sounds like the AnimatedContent wraps everything The card itself can have a
sharedBounds
The icon itself can have
sharedElement
And that should make it so that the icon does not fade. Now I see you're doing
sharedBounds
but then removing the transition instead of just doing
sharedElement
on the icon directly. If you just do this replacement, what breaks? Btw I am also not very experienced using these APIs, so it's also much trial and error while I am still learning to use them.
z
final_final_92924: I think it looks even smoother "IRL" seems like an FPS issue with screen capture.
โค๏ธ 1
๐ŸŽ‰ 1
Separate sharedX โœ… Im using 2 separate AnimatedVisibility blocks, using AnimatedContent is almost as good but doesnt quite get there. Ill try to use sharedBounds on pill and element on icon!
SharedBounds on card makes the icon use its bounds during the transition, i.e. icon becomes larger then smaller - but it does fix the fade issue ๐Ÿ˜›
Hmm, using AnimatedContent kinda works now!
Ill share the code here before I have to go. This works pretty well. I guess Im not super happy about using sharedBounds though, seems that theres some knowledge missing there. But Im glad that AnimatedContent works! Now I just have to incorporate that into the root (this is why "2:00" becomes "0:00" when stopping the timer, ideally it should remain "2:00" until it has faded out. Comments welcome. Thank you both for your help. Ill probably respond in the evening again ๐Ÿ™‚
Copy code
@Composable
fun Rest(
    active: Boolean,
    progress: Float,
    expiring: Boolean,
    onStart: () -> Unit,
    onStop: () -> Unit,
    onOpen: () -> Unit,
    label: String,
    modifier: Modifier = Modifier,
    icon: Icon = Theme.icons.rest,
) {
    SharedTransitionLayout(modifier) {
        Box(
            modifier = Modifier.fillMaxHeight(),
            contentAlignment = Center,
            content = {
                TransitionContent(active, Crossfade, alignment = CenterEnd) { active ->
                    val iconModifier =
                        Modifier.sharedBounds(
                            sharedContentState = rememberSharedContentState("icon"),
                            animatedVisibilityScope = this,
                            enter = EnterTransition.None,
                            exit = ExitTransition.None,
                        )

                    if (active) {
                        Active(
                            iconModifier = iconModifier,
                            icon = icon,
                            progress = progress,
                            expiring = expiring,
                            label = label,
                            onOpen = onOpen,
                            onStop = onStop,
                        )
                    } else {
                        Idle(
                            iconModifier = iconModifier,
                            icon = icon,
                            onOpen = onOpen,
                            onStart = onStart,
                        )
                    }
                }
            },
        )
    }
}
s
ideally it should remain "2:00" until it has faded out
I'd pass the state itself into the
AnimatedContent
entirely, instead of just if it should show or not. That way inside the lambda you'll get the state as
it
which will still be what it was before it was flipped while the animation is playing. Which is why I advocated for AnimatedContent in the first place It's what's described here https://developer.android.com/develop/ui/compose/animation/composables-modifiers#animatedcontent where animatedContent gives you the power of still holding onto the "old" state so that the UI does not magically jump to the new state as it also animates out.
z
Yeah exactly my thinking, I just couldnt do that (afaik) when I was using AnimatedVisibility earlier. I just have to refactor the states into something I can use with AnimatedContent!
yes black 1
That works too, just extracted stuff into RestState, nothing exciting really. I guess now the only thing that kind of remains is understanding if the sharedBounds stuff is correct usage, or how it is intended to be used. Just like youve mentioned, it doesnt seem quite right to do it this way.
Coming back to this after the weekend, I've noticed that when I use AnimatedContent instead of visibility, the shakiness is present! This makes it feel more like a bug, or unsupported use case. @Doris Liu would you mind taking a look? In the interest of your time, if you prefer I can submit a bug report instead. This thread probably has all the necessary info for both!
I do wonder why its such a hassle to get that aspect of it right. I dont even really care that the content bounds translate so smoothly, as I just want them to be centered in the parent (which is static in height).
d
Re: shakiness, I suspect it's the result of multiple sources directly or indirectly causing offsets to change simultaneously, the sum of which produces the odd shaking look. More specifically, the AnimatedContent is change its size, causing it to be positioned different each frame in its parent, meanwhile its children are changing positions through enter/exit transitions. The children get a combination of offset changes. Shared elements are supposed to bypass all these changes from parent layouts by animating to the lookahead position. But its continuousness is only within in
SharedTransitionLayout
. If the
SharedTransitionLayout
is being offseted by its parents, you may still see some strange movement patterns. It's worth logging the position of
SharedTransitionLayout
to check if that's the culprit
z
Thanks for looking into it! I tried logging the position using
Modifier.onGloballyPositioned
and `positionInRoot`: โ€ข The SharedTransitionLayout only moves horizontally (which makes sense, the pill is wider than just the icon). โ€ข However, if I do the same test on the icon itself, it moves 1 pixel vertically during the transition. This seems to be due to IconButton using
Modifier.minimumInteractiveComponentSize
. But.. even if I ditch that and the icon doesnt move vertically, the shake is still there. My animation sits inside a Toolbar with static height. Heres my "current" code for the animation itself.
Copy code
SharedTransitionLayout(modifier) {
    Box(
        modifier = Modifier.fillMaxHeight(),
        contentAlignment = Center,
        content = {
            TransitionContent( // AnimatedContent
                targetState = state,
                effect = /* fadeIn + expandHorizontally togetherWith fadeOut + shrinkHorizontally */
                alignment = CenterEnd,
                content = { current ->
                    val iconModifier =
                        Modifier.sharedBounds( // SharedBounds/SharedElement: Same behavior in terms of shakiness
                            animatedVisibilityScope = this,
                            sharedContentState = rememberSharedContentState("icon"),
                            enter = EnterTransition.None,
                            exit = ExitTransition.None,
                        )

                    when (current) {
                        is Active ->
                            Active(
                                iconModifier = iconModifier,
                                ..
                            )

                        Inactive ->
                            Inactive(
                                iconModifier = iconModifier,
                                ..
                            )
                    }
                },
            )
        },
    )
}
d
Thanks for the details Zoltan. Any chance you could give me a minimal repro case for the shakiness? I'd love to dig into it.
z
Of course! Notice how the red "pill" shakes during the animation.
Copy code
private fun <T> spec() = tween<T>(5_000)

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun Bug() {
    val expanded by produceState(false) {
        while (isActive) {
            delay(5_000 + 500)
            value = !value
        }
    }

    // Scaffold
    Box(Modifier.fillMaxSize().background(Color.Black), contentAlignment = Center) {
        // Toolbar / TopAppBar
        Row(
            modifier = Modifier.requiredHeight(60.dp).fillMaxWidth().background(Color.Gray),
            verticalAlignment = CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            content = {
                Box(Modifier.weight(1f))

                SharedTransitionLayout {
                    // The box doesnt seem to make a difference, same alignment as row, but including
                    // because my own code had it last time I ran it
                    Box(
                        modifier = Modifier.fillMaxHeight(),
                        contentAlignment = Center,
                        content = {
                            AnimatedContent(
                                targetState = expanded,
                                contentAlignment = Alignment.CenterEnd,
                                transitionSpec = {
                                    fadeIn(spec()) +
                                        expandHorizontally(
                                            animationSpec = spec(),
                                            clip = false,
                                        ) togetherWith fadeOut(animationSpec = spec()) +
                                        shrinkHorizontally(
                                            animationSpec = spec(),
                                            clip = false,
                                        ) using SizeTransform(false) { _, _ -> spec() }
                                },
                                content = { expanded ->
                                    val iconModifier =
                                        Modifier.sharedBounds(
                                            sharedContentState =
                                                rememberSharedContentState(
                                                    "icon",
                                                ),
                                            // None used so that the icon doesnt fade during animation
                                            enter = EnterTransition.None,
                                            exit = ExitTransition.None,
                                            animatedVisibilityScope = this,
                                            // This is just for debugging purposes
                                            boundsTransform = { _, _ ->
                                                spec()
                                            },
                                        )

                                    if (expanded) {
                                        Row(
                                            modifier =
                                                Modifier.background(Color.Red).padding(
                                                    horizontal = 8.dp,
                                                    vertical = 4.dp,
                                                ),
                                            verticalAlignment = CenterVertically,
                                            content = {
                                                Icon(24.dp, modifier = iconModifier)

                                                BasicText(
                                                    "1:23",
                                                    color = { Color.Cyan },
                                                    modifier = Modifier.padding(start = 4.dp),
                                                )
                                            },
                                        )
                                    } else {
                                        IconButton(iconModifier)
                                    }
                                },
                            )
                        },
                    )
                }

                // Just for sanity, there are other iconbuttons in the toolbar and they shoul match in size etc
                repeat(3) {
                    IconButton(color = Magenta)
                }
            },
        )
    }
}

@Composable
private fun IconButton(
    iconModifier: Modifier = Modifier,
    color: Color = Blue,
) {
    Box(Modifier.requiredSize(40.dp), contentAlignment = Center) {
        Icon(color = color, modifier = iconModifier)
    }
}

@Composable
private fun Icon(
    size: Dp = 24.dp,
    color: Color = Blue,
    modifier: Modifier = Modifier,
) {
    Box(
        modifier = modifier.requiredSize(size).clip(CircleShape).background(color),
    )
}
If I replace AnimatedContent with 2x AnimatedVisibility (placed in a row) then the shakiness seems to be gone. From what I can tell, this means that the shakiness is likely due to the bounds changing between states in AnimatedContent? Maybe thats what you were referring to earlier as well. I cant really tell how Id get AnimatedContent to work for this purpose - Id need to specify a fixed size for the contents in order for them to animate smoothly? Obviously I cant do that ๐Ÿ˜… Thanks for taking a look, code below for visibility instead of animated content. Id love to use animated content as it makes my case easier (ironically).
Copy code
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun SharedTransitionScope.Content(expanded: Boolean) {
    val enter =
        fadeIn(spec()) +
            expandHorizontally(
                animationSpec = spec(),
                clip = false,
            )
    val exit =
        fadeOut(animationSpec = spec()) +
            shrinkHorizontally(
                animationSpec = spec(),
                clip = false,
            )

    Row(
        modifier = Modifier.fillMaxHeight(),
//                        contentAlignment = Center,
        verticalAlignment = CenterVertically,
        content = {
            AnimatedVisibility(!expanded, enter = enter, exit = exit, content = {
                val iconModifier =
                    Modifier.sharedBounds(
                        sharedContentState =
                            rememberSharedContentState(
                                "icon",
                            ),
                        // None used so that the icon doesnt fade during animation
                        enter = EnterTransition.None,
                        exit = ExitTransition.None,
                        animatedVisibilityScope = this,
                        // This is just for debugging purposes
                        boundsTransform = { _, _ ->
                            spec()
                        },
                    )

                Icon(modifier = iconModifier)
            })
            AnimatedVisibility(
                expanded,
                enter = enter,
                exit = exit,
            ) {
                val iconModifier =
                    Modifier.sharedBounds(
                        sharedContentState =
                            rememberSharedContentState(
                                "icon",
                            ),
                        // None used so that the icon doesnt fade during animation
                        enter = EnterTransition.None,
                        exit = ExitTransition.None,
                        animatedVisibilityScope = this,
                        // This is just for debugging purposes
                        boundsTransform = { _, _ ->
                            spec()
                        },
                    )

                Row(
                    modifier =
                        Modifier.background(Color.Red).padding(
                            horizontal = 8.dp,
                            vertical = 4.dp,
                        ),
                    verticalAlignment = CenterVertically,
                    content = {
                        Icon(modifier = iconModifier)

                        BasicText(
                            "1:23",
                            color = { Color.Cyan },
                            modifier = Modifier.padding(start = 4.dp),
                        )
                    },
                )
            }
        },
    )
}
d
Wonderful! Thank you @Zoltan Demant for the repro. It's very helpful. Let me investigate this. ๐Ÿ™‚
Please feel free to follow the updates here: https://issuetracker.google.com/402788131
BTW, which version of compose lib are you using, Zoltan?
z
Awesome, so it was just a bug in the end.. I seriously thought I was doing something wrong! Ill go with AnimatedVisibility until its resolved.
Copy code
compose-multiplatform = "1.7.3"
๐Ÿ˜ 1
d
I actually couldn't reproduce it in compose 1.8. Would you mind trying out the repro with the latest 1.8 compose lib, and let me know if I'm missing something?
๐Ÿ‘€ 1
z
I spoke too soon ๐Ÿคฆ๐Ÿฝโ€โ™‚๏ธ Its still happening with
compose = "1.8.0-alpha04"
just using the repro sample. Maybe you were using a more recent 1.8 release than I am?
I also want to add that with 1.8-alpha04, I can use Modifier.sharedElement without the icon blinking in the transition! Which is awesome. I guess that some changes were made so that shared elements themselves arent affected by the AnimatedContent animation?
๐Ÿ‘€ 1
d
Let me check the repro again with a different duration scale. In the meantime, could you share a screen recording of what you observe when you repro? I might have missed something.
๐Ÿ‘๐Ÿฝ 1
๐Ÿ‘ 1
z
Sure, but I'll be stuck in nature over the coming days! The red box around the timer icon (in expanded state) shakes vertically during the animation for me. If text explanation is not good enough, stay tuned until Sunday/Monday for video! ๐Ÿ’ช๐Ÿฝ
๐ŸŒด 1
d
Ok, I'm able to repro the shakiness now. It wasn't noticeable on device, but more pronounced in the recording when I enlarge it. No need to record your repro, Zoltan. Thanks for reporting it.
๐ŸŒŸ 1
Ok, this seems like a rounding issue caused by our size & position being integer based. The summary is that when you center a layout in parent, you may trigger an integer rounding depending the size of that layout. If that size is animating, you'll definitely hit rounding in some but not all frames. Hence the shaking.
โญ 1
๐Ÿ‘€ 2