androidx / constraintlayout

ConstraintLayout is an Android layout component which allows you to position and size widgets in a flexible way
Apache License 2.0
1.08k stars 175 forks source link

[Compose] [MotionLayout] Text with animated fontSize returns to its original bounding box after recomposition? #507

Open CalamityDeadshot opened 2 years ago

CalamityDeadshot commented 2 years ago

I have the following ConstraintSets for the MotionLayout:

@Composable
private fun StartConstraintSet() = ConstraintSet(
    """ {
    title: {
        top: ['parent', 'top'],
        start: ['parent', 'start'],
        end: ['parent', 'end'],
        bottom: ['content', 'top', 24],
        custom: {
            fontSize: 32,
            fontWeight: 400
        }
    },
    options: { 
        end: ['parent', 'end', 0],
        bottom: ['content', 'top', 15]
    },
    content: {
        bottom: ['parent', 'bottom', 0]
    }
} """
)

@Composable
private fun EndConstraintSet() = ConstraintSet(
    """ {
    title: {
        top: ['parent', 'top', 0],
        start: ['parent', 'start', 16],
        bottom: ['content', 'top', 0],
        custom: {
            fontSize: 20,
            fontWeight: 500
        }
    },
    options: { 
        end: ['parent', 'end', 0],
        bottom: ['content', 'top', 0],
        top: ['parent', 'top', 0]
    },
    content: {
        bottom: ['parent', 'bottom', 0]
    }

} """
)

and the following scene:

MotionLayout(
    start = StartConstraintSet(),
    end = EndConstraintSet(),
    progress = if (swipingState.progress.to == SwipingStates.COLLAPSED) swipingState.progress.fraction else 1f - swipingState.progress.fraction,
    modifier = Modifier
        .fillMaxWidth()
        .height(height)
) {
    Text(
        text = title,
        modifier = Modifier
            .layoutId("title")
            .wrapContentWidth(unbounded = true) // This is necessary because title's last letter was being clipped in both start and end
            .wrapContentHeight(),
        color = MaterialTheme.colors.onSurface,
        fontWeight = FontWeight(motionInt("title", "fontWeight")),
        fontSize = motionFontSize("title", "fontSize")
    )
    OptionsRow(
        options = options,
        modifier = Modifier
            .layoutId("options")
            .fillMaxWidth(.5f)
            .padding(end = 16.dp)
    )

    content(
        Modifier
            .layoutId("content")
    )

}

This is a correctly rendered StartConstraintSet: image And this is a correctly rendered EndConstraintSet: image

However, after the whole app bar is recomposed, title obtains what looks like a margin: image

This doesn't happen when simply scaling the text, so it must be its bounding box returning to 32sp value from before the transformation. I cannot explain this behavior with anything else.

oscar-ad commented 2 years ago

Which version of the library are you using?

Have you tried removing the wrapContentWidth/Height modifiers? Ie: Just Modifier.layoutId("title")

MotionLayout will default to wrapContent.

CalamityDeadshot commented 2 years ago

I am using constraintlayout-compose:1.0.0. I put wrapContentWidth on the text because without it another problem arises: the text is being inconsistently clipped. image Here I assigned "Long text" as title's value: image image It is rendered correctly after recomposition. I believe it belongs to a separate issue.

But yes, margin does not appear if I remove wrapContentWidth.

oscar-ad commented 2 years ago

Taking another look at this, not sure if this is what you intended but it seems to do the trick in terms of the expected layout.

@Preview
@Composable
private fun Issue507Preview() {
    var toEnd by remember { mutableStateOf(false) }
    val progress by animateFloatAsState(
        targetValue = if(toEnd) 1f else 0f,
        tween(2500)
    )
    Column {
        Button(onClick = {
            toEnd = !toEnd
        }) {
            Text(text = "Run")
        }
        Issue507(progress = progress)
    }
}

@OptIn(ExperimentalMotionApi::class)
@Composable
fun Issue507(progress: Float) {
    MotionLayout(
        modifier = Modifier
            .background(Color.LightGray)
            .fillMaxWidth()
            .height(lerp(150.dp, 50.dp, progress)),
        motionScene = MotionScene(content = """
                {
                  ConstraintSets: {
                    start: {
                        title: {
                            top: ['parent', 'top'],
                            start: ['parent', 'start'],
                            end: ['parent', 'end'],
                            bottom: ['options', 'top', 24],
                            custom: {
                                fontSize: 32,
                                fontWeight: 400
                            }
                        },
                        options: { 
                            end: ['parent', 'end', 0],
                            bottom: ['content', 'top', 15]
                        },
                        content: {
                            width: 'spread', height: 'wrap',
                            start: ['parent', 'start'],
                            end: ['parent', 'end'],
                            bottom: ['parent', 'bottom', 0]
                        }
                    },
                    end: {
                        title: {                            
                            top: ['parent', 'top', 0],
                            start: ['parent', 'start', 16],
                            bottom: ['content', 'top', 0],
                            custom: {
                                fontSize: 20,
                                fontWeight: 500
                            }
                        },
                        options: { 
                            end: ['parent', 'end', 0],
                            bottom: ['content', 'top', 0],
                            top: ['parent', 'top', 0]
                        },
                        content: {
                            width: 'spread', height: 'wrap',
                            start: ['parent', 'start'],
                            end: ['parent', 'end'],
                            bottom: ['parent', 'bottom', 0]
                        }
                    }
                  },
                  Transitions: {
                    default: {
                      from: 'start',
                      to: 'end'
                    }
                  }
                }
            """.trimIndent()),
        progress = progress
    ) {
        Text(
            text = "This is a very long text",
            modifier = Modifier
                .layoutId("title")
                .wrapContentWidth(unbounded = true), // Seems to measure the text more accurately
            color = MaterialTheme.colors.onSurface,
            maxLines = 1, // maxLines and no softWrap help a bit with clipping
            softWrap = false,
            fontWeight = FontWeight(motionInt("title", "fontWeight")),
            fontSize = motionFontSize("title", "fontSize")
        )
        Options(
            modifier = Modifier
                .layoutId("options")
                .background(Color.Blue)
        )
        Tabs(
            Modifier
                .layoutId("content")
                .background(Color.Red)
        )
    }
}

@Composable
private fun Options(modifier: Modifier = Modifier) {
    Row(modifier = modifier) {
        Icon(imageVector = Icons.Default.Settings, contentDescription = null)
        Icon(imageVector = Icons.Default.Notifications, contentDescription = null)
        Icon(imageVector = Icons.Default.Filter, contentDescription = null)
        Icon(imageVector = Icons.Default.Search, contentDescription = null)
    }
}

@Composable
private fun Tabs(modifier: Modifier = Modifier) {
    Row(modifier = modifier,horizontalArrangement = Arrangement.SpaceEvenly) {
        Text(text = "All")
        Text(text = "My")
    }
}

There seems to be two issues while animating the text that results in the clipping.

In any case, thanks for the report, at least for the clipping I understand a little better what's happening.