Deepak Gahlot

    Deepak Gahlot

    1 year ago
    🧵 I have one query related to "transitionState" for animations in jetpack compose. I have created an expandable list which on the icon click expands and collapse, which certain animations. What i'm seeing is that the Composable function which shows expanded content, is being executed 4 times. And that is the same number of transition i'm applying.
    Is this something expected ?
    @ExperimentalFoundationApi
    @ExperimentalAnimationApi
    @Composable
    fun ExpandableCard(
        card: ItemsItem,
        questions: List<ItemsItem>,
        qResponse: Response
    ) {
        val expanded = remember { mutableStateOf(false) }
    
        val transitionState = remember {
            MutableTransitionState(expanded.value).apply {
                targetState = !expanded.value
            }
        }
        val transition = updateTransition(transitionState)
        val cardPaddingHorizontal by transition.animateDp({
            tween(durationMillis = EXPAND_ANIMATION_DURATION)
        }) {
            if (expanded.value) 10.dp else 10.dp
        }
        val cardElevation by transition.animateDp({
            tween(durationMillis = EXPAND_ANIMATION_DURATION)
        }) {
            if (expanded.value) 10.dp else 4.dp
        }
        val cardRoundedCorners by transition.animateDp({
            tween(
                durationMillis = EXPAND_ANIMATION_DURATION,
                easing = FastOutSlowInEasing
            )
        }) {
            if (expanded.value) 0.dp else 5.dp
        }
        val arrowRotationDegree by transition.animateFloat({
            tween(durationMillis = EXPAND_ANIMATION_DURATION)
        }) {
            if (expanded.value) 0f else 180f
        }
        Card(
            contentColor = colorResource(id = R.color.white),
            elevation = cardElevation,
            shape = RoundedCornerShape(cardRoundedCorners),
            modifier = Modifier
                .fillMaxWidth()
                .padding(
                    horizontal = cardPaddingHorizontal,
                    vertical = 8.dp
                )
        ) {
            Column {
                Row(
                    modifier = Modifier
                        .background(Color.White)
                        .padding(20.dp, 10.dp, 0.dp, 10.dp)
                        .fillMaxWidth()
                        .animateContentSize(),
                    horizontalArrangement = Arrangement.SpaceBetween
                ) {
                    @Suppress("DEPRECATION")
                    ConstraintLayout(
                        modifier = Modifier.fillMaxWidth()
                    ) {
                        val (textField, iconImage) = createRefs()
                        Text(
                            text = card.qualifiedNumber + " " + card.label,
                            modifier = Modifier.constrainAs(textField) {
                            }
                                .padding(0.dp, 10.dp, 0.dp, 0.dp)
                                .fillMaxWidth(),
                            color = colorResource(id = R.color.black)
                        )
                        IconButton(
                            onClick = { if(expanded.value) { expanded.value = false } else { expanded.value = true } },
                            modifier = Modifier.constrainAs(iconImage) {
                                end.linkTo(parent.absoluteRight)
                            },
                            content = {
                                Icon(
                                    painter = painterResource(id = R.drawable.ico_expand_arrow),
                                    contentDescription = "Expandable Arrow",
                                    modifier = Modifier.rotate(arrowRotationDegree),
                                    tint = Color(R.color.black)
                                )
                            }
                        )
                    }
                }
                //Add the expandable content here
                ExpandableContent(
                    questions as ArrayList<ItemsItem>,
                    qResponse, visible = expanded.value,
                    initialVisibility = expanded.value
                )
            }
        }
    
    }
    this is the Code block i have written and the Card composable which is the parent is being executed for times, which i tap on the Icon for expanding the Layout. It is working as expected , the logic for expand - collapse is working flawless, but since it is calling the child composable number of times, i'm processing a huge list of data that is also being called multiple times which is causing performance issues on the UI (edited)
    Doris Liu

    Doris Liu

    1 year ago
    Sorry for the late reply. I played around with the code snippet and was able to reproduce the additional recompositions. It's not immediately clear to me what's causing the recompositions. Could you file a bug here: https://issuetracker.google.com/issues?q=componentid:612128 with the code snippet? I'll investigate further. 🙂
    Actually, after taking a further look, in the
    Transition.animateFloat/animateDp
    call, the
    if (expanded.value) 0f else 180f
    is causing additional composition. In that lambda it's recommended to use the targetState that is passed to the lambda:
    if (it) 0f else 100f
    so that external state change doesn't directly cause animations to recompose.
    Deepak Gahlot

    Deepak Gahlot

    1 year ago
    so should we do that for all the other animations as well.
    Doris Liu

    Doris Liu

    1 year ago
    Yes. Please let me know if you still have issues after changing that. We should probably add a lint for this. 🤔
    Deepak Gahlot

    Deepak Gahlot

    1 year ago
    what i understood from your comment, is that we need to use targetState instead of using expanded.value
    Sure i will try and let you know, thanks for the quick help. :thank-you:
    Doris Liu

    Doris Liu

    1 year ago
    Not the targetState of
    Transition
    , but targetState that is passed to the lambda. I.e. I'd change this
    {
            if (expanded.value) 0f else 180f
        }
    to
    {it ->
            if (it) 0f else 180f
        }
    This is because when we support seeking, we may pass different
    targetState
    than the actual one to query both the start and end values for seeking purposes. 🙂
    Deepak Gahlot

    Deepak Gahlot

    1 year ago
    got it, make sense.
    Doris Liu

    Doris Liu

    1 year ago
    Created an issue to add lint as a follow-up in case you are interested: https://issuetracker.google.com/181195383
    BTW, if you have android studio canary8, I'd encourage you to check the animations in the animation tools (as the tooling uses seeking under the hood). This is what the animation tools look like: https://developer.android.com/jetpack/compose/tooling#animations
    Deepak Gahlot

    Deepak Gahlot

    1 year ago
    This is cool. I was in the process of updating the project to beta:01. Sure will try this out thanks again