https://kotlinlang.org logo
#compose
Title
# compose
u

ursus

10/14/2023, 11:39 PM
How is
Crossfade
different from
AnimatedContent
? seems weird to have such composable, when crossfade animation is just one example of a transition, you can input into
AnimatedContent
Or why is it special?
f

Francesc

10/14/2023, 11:50 PM
Crossfade
is to swap between different composables.
AnimatedContent
is to animate adding/removing or chaging the size of content
u

ursus

10/14/2023, 11:51 PM
hm? how is swapping between 2 different from adding/removing
f

Francesc

10/14/2023, 11:51 PM
you may have a list of items and you add 1 more to the list, that's animated content. You have my have a loading spinner and a list of loaded results that you swap, that's Crossfade
u

ursus

10/14/2023, 11:53 PM
still not getting it
Copy code
AnimatedContent(someEnum) {
	when (it) {
		FOO -> FooScreen()
		BAR -> BarScreen()
	}
}
this is how's it used in the docs
and they even specify the transition as I mentioned originally
Copy code
transitionSpec = {
        fadeIn(
            animationSpec = tween(3000)
        ) togetherWith fadeOut(animationSpec = tween(3000))
    },
f

Francesc

10/14/2023, 11:55 PM
you can use animatedContent to replicate crossfade. You could think of crossfade as a custom-made animatedContent for the specific case of swapping composables
AnimatedContent is more generic
u

ursus

10/14/2023, 11:56 PM
hence my question, why have specific api for
Crossfade
, and if you look at the sources, its not convenience api for
Copy code
AnimatedContent(
    state,
    transitionSpec = {
        fadeIn(
            animationSpec = tween(3000)
        ) togetherWith fadeOut(animationSpec = tween(3000))
    },
which is what one would expect (even though if it were, not worth it imo)
f

Francesc

10/14/2023, 11:58 PM
I don't know the answer to that, but if I had to guess, I'd say that swapping content is common enough (like the loading/loaded scenario) that it warrants a simpler API just for that usecase
u

ursus

10/14/2023, 11:58 PM
I maybe slow but how is swapping A and B, different than removing A and then inserting B
semantically to me that is the same thing
f

Francesc

10/14/2023, 11:59 PM
it's not, but crossfade, as the name indicates, fades one out and the other in, as part of the swap. It's not "remove A and show B".
u

ursus

10/15/2023, 12:00 AM
AnimatedContent does the very same, they just add scaling to the fading
Copy code
transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = {
    (fadeIn(animationSpec = tween(220, delayMillis = 90)) +
        scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)))
        .togetherWith(fadeOut(animationSpec = tween(90)))
},
this is the default
image.png
f

Francesc

10/15/2023, 12:01 AM
I don't know if this is implied, but I always use crossfade when I have composables of the same size
u

ursus

10/15/2023, 12:02 AM
whats funny though, is that the sample as how to use
AnimatedContent
plus just fadeIn+fadeOut in the docs, doesnt work
it still adds some stupid scaling as well
f

Francesc

10/15/2023, 12:03 AM
it doesn't honor the animation spec you pass in?
u

ursus

10/15/2023, 12:03 AM
no, it acts weird, here
f

Francesc

10/15/2023, 12:06 AM
could you change it like this?
Copy code
AnimatedVisibility(
    visible = value != null
) {
    Text(value.orEmpty())
)
u

ursus

10/15/2023, 12:09 AM
no that does some odd clipping
Screen Recording 2023-10-15 at 02.09.58.mov
I mean, about the AnimatedContent, I know what's it trying to do, its trying to animated the bounds as well, so it goes from 0 to whatever the width is
if I do it like this
Copy code
AnimatedContent(
    targetState = error,
    transitionSpec = { fadeIn() togetherWith fadeOut() },
    label = ""
) {
    if (it != null) {
        Text(
            text = it,
            color = MaterialTheme.colors.onErrorBackground,
            style = MaterialTheme.typography.caption,
            maxLines = 1
        )
    } else {
        Box(modifier = Modifier.width(120.dp).height(16.dp))
    }
}
then it looks the way I intended, but obviously I don't know the size, and knowing it defeats the purpose of the whole thing..
about your
AnimatedVisibility
I spoke to soon, its because thats what the animation is, if I override it to just fadeinout
Copy code
AnimatedVisibility(visible = error != null, enter = fadeIn(), exit = fadeOut()) {
                Text(
                    text = error ?: "",
                    color = MaterialTheme.colors.onErrorBackground,
                    style = MaterialTheme.typography.caption,
                    maxLines = 1
                )
            }
then it works as expected
f

Francesc

10/15/2023, 12:14 AM
yes, you need this
Copy code
AnimatedVisibility(
    visible = value != null,
    enter = fadeIn(),
    exit = fadeOut(),
)
u

ursus

10/15/2023, 12:14 AM
BUT, this is not a good solution anyways, since its just a coincidende that its a string I can elvis that to
""
imagine this some data class type..
all I'm tring to do, is animate this
Copy code
if (someState != null) {
	Hint(someState)
}
why is this so hard..
again, Crossfade handles it .. but what if I want a slide inout not a fade..then im SOL
f

Francesc

10/15/2023, 12:19 AM
I don't know exactly what you need, but you can definitively slide things in and out, there are several GIFs in the link I posted showing different transition animations
u

ursus

10/15/2023, 12:21 AM
so how would you do this then?
Copy code
if (someState != null) {
	Hint(someState)
}
like this?
Copy code
AnimatedContent(
	targetState = someState,
	transitionSpec = { slideIn() togetherWith slideOut() }
) {
	if (it != null) {
		Hint(it)	
	}
}
f

Francesc

10/15/2023, 12:23 AM
Maybe wrap
SomeState
as a sealed class,
Copy code
sealed interface MyWrapper {
   data object IDontHaveAValue : MyWrapper
   data class MyState(val ...) : MyWrapper
}

AnimatedVisibility(
    visible = state.myWrapper is MyWrapper.MyState
) {
    MyStateComposable(state.myWrapper)
}
u

ursus

10/15/2023, 12:25 AM
but in practise most likely
MyStateComposable
will take
MyState
as argument so then you need a cast, and that will crash
Copy code
if (someState != null) {
	Hint(someState)
}
this is what you're trying to animate, simple requirement
f

Francesc

10/15/2023, 12:27 AM
that's what AnimatedContent is supposed to be for, when you change the content of your container
u

ursus

10/15/2023, 12:27 AM
sure, but try it, it wont behave as you expect
here, I did it for you
Copy code
AnimatedContent(
                targetState = error,
                transitionSpec = {
                    slideInHorizontally() togetherWith slideOutHorizontally()
                }, label = ""
            ) {
                if (it != null) {
                    Text(
                        text = it,
                        color = MaterialTheme.colors.onErrorBackground,
                        style = MaterialTheme.typography.caption,
                        maxLines = 1
                    )
                }
            }
is this what you'd expect?
you can see the height changing as well
f

Francesc

10/15/2023, 12:36 AM
this works reasonably well
Copy code
@Preview(widthDp = 420, heightDp = 360)
@Composable
fun PreviewAnimatedContent() {
    SampleTheme {
        var visible by remember { mutableStateOf(false) }

        Surface(
            color = MaterialTheme.colorScheme.background,
        ) {
            Column(
                modifier = Modifier.padding(all = 16.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                AnimatedContent(targetState = visible, label = "animate") {
                    if (it) {
                        Text(
                            text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris tempor nibh vitae est tincidunt, quis ornare lacus pulvinar. Donec non pretium metus. Fusce in libero facilisis, scelerisque magna id, maximus arcu. Vestibulum pellentesque ex nec dui condimentum, sit amet iaculis sapien porttitor. Nunc molestie est non congue lacinia."
                        )
                    }
                }
                Button(onClick = { visible = !visible }) {
                    Text(
                        text = "Toggle"
                    )
                }
            }
        }
    }
}
u

ursus

10/15/2023, 12:37 AM
can you override the transition to just fadein+out?
f

Francesc

10/15/2023, 12:41 AM
it seems to scale in from the middle
this is how I changed it
Copy code
AnimatedContent(
    targetState = visible,
    transitionSpec = {
        fadeIn().togetherWith(fadeOut())
    },
    label = "animate",
u

ursus

10/15/2023, 12:44 AM
yea which is exactly what I'm seeing as well,
do you consider that correct behavior? I dont..
f

Francesc

10/15/2023, 12:44 AM
it's a bit weird, the first iteration is correct, later is starts scaling, I wonder if it's an emulator thing. Let me video the 1st iteration with a slower animation
Screencast from 2023-10-14 17-44-56.webm
u

ursus

10/15/2023, 12:45 AM
My interpretation is that it animates from a view of size 1000, 500 to one of size 0,0
i.e. it animates the bounds as well
f

Francesc

10/15/2023, 12:46 AM
it does, yes, you can see the text shrinking
u

ursus

10/15/2023, 12:47 AM
if thats the behavior animatedContent wants, sure, but then there is no api for this .. sure one could do CrossFade .. but what if I want a horizontal slide in and out.. then im SOL
f

Francesc

10/15/2023, 12:47 AM
sorry, I need to bail, maybe somebody from the Google team can shed some light
u

ursus

10/15/2023, 12:48 AM
would tagging someone work?
f

Francesc

10/15/2023, 12:48 AM
that's usually frown upon
especially on a weekend
u

ursus

10/15/2023, 12:48 AM
😄 oh well, thanks anyways
👍 1
z

Zoltan Demant

10/15/2023, 6:11 AM
For the show value if not null case, this is what I do. Its effectively similar to doing the check yourself, but when going through null -> value -> null, it will still show value until it has fully faded out. The function names differ from what you see in compose, just ignore that, its AnimatedContent under the hood.
Copy code
@Composable
inline fun <T> TransitionValue(
    value: T?,
    modifier: Modifier = Modifier,
    effect: Effect = Fade,
    crossinline content: @Composable (T) -> Unit,
) {
    val ref = remember {
        Ref<T>()
    }.also { ref ->
        ref.value = value ?: ref.value
    }

    TransitionVisibility(
        modifier = modifier,
        visible = value != null,
        effect = effect,
        content = {
            ref.value?.let { value ->
                content(value)
            }
        },
    )
}
v

vide

10/15/2023, 9:38 AM
You can customize the size transform with
using
, eg.
Copy code
transitionSpec = { fadeIn() togetherWith fadeOut() using null }
3
u

ursus

10/15/2023, 4:05 PM
@vide finally! thanks!
@Zoltan Demant thats intresting, but if you're using AnimatedContent inside TransitionVisibility, then why the remembering bit? why not just
Copy code
AnimatedContent(value) {
   if (it != null) {
       Something(it)
   }
}
z

Zoltan Demant

10/15/2023, 4:42 PM
@ursus my bad, it's using animated visibility under the hood. It's been a while since I wrote the code but I'm sure there was some edge case that made me use this over animated content!
@ursus I ran into the why this morning: AnimatedContent will animate between values, whereas AnimatedVisibility will just animate between value exists or not. The latter is the behavior Id expect from a function like this, but thats up to you! Also, you can probably use AnimatedContent with key = { it != null } but that wasnt available when I wrote this 🙂
6 Views