constraintlayout icon indicating copy to clipboard operation
constraintlayout copied to clipboard

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

Open CalamityDeadshot opened this issue 3 years ago • 3 comments

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.

CalamityDeadshot avatar Feb 03 '22 23:02 CalamityDeadshot

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.

oscar-ad avatar Feb 05 '22 00:02 oscar-ad

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.

CalamityDeadshot avatar Feb 05 '22 01:02 CalamityDeadshot

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.

  • Text bounds not calculated properly during animation
  • At some point during the animation the location of the starting position seems to collapse to 0,0 instead of staying around the middle of the layout

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

oscar-ad avatar May 13 '22 23:05 oscar-ad