androidandroid-jetpack-compose

Why doesn't the Text Composable fit the size that the text occupies?


I have this code, in MessageItem is the Surface and also inside that Surface is the Text.

I tried with .wrapContentSize() but it didn't work

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChatScreen(
    chatId: String?,
    onBack: () -> Unit
) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(
                        text = "Chat with Alice"
                    )
                }
            )
        },
        bottomBar = {
            SendMessageBox()
        }
    ) { innerPadding ->
        ListOfMessages(modifier = Modifier.padding(innerPadding))
    }
}
@Composable
fun ListOfMessages(modifier: Modifier = Modifier) {

    LazyColumn(
        modifier = modifier.fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(getFakeMessages()) { message ->
            MessageItem(message)
        }
    }
}
@Composable
fun MessageItem(message: Message) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .then(if (message.isMine) Modifier.padding(start = 48.dp) else Modifier),
        horizontalArrangement = if (message.isMine) Arrangement.End else Arrangement.Start
    ) {
        if (!message.isMine) {
            Avatar(
                imageUrl = message.senderAvatar,
                size = 40.dp,
                contentDescription = "${message.senderName}'s avatar"
            )
            Spacer(modifier = Modifier.width(8.dp))
        }
        Column {
            if (message.isMine) {
                Spacer(modifier = Modifier.height(8.dp))
            } else {
                Text(
                    text = message.senderName,
                    fontWeight = FontWeight.Bold
                )
            }
            when (val content = message.messageContent) {
                is MessageContent.TextMessage -> {
                    Surface(
                        shape = RoundedCornerShape(8.dp),
                        color = if (message.isMine) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary
                    ) {
                        Text(
                            text = content.message,
                            modifier = Modifier.padding(8.dp),
                            color = if (message.isMine) MaterialTheme.colorScheme.onPrimary else Color.White
                        )
                    }
                }

                is MessageContent.ImageMessage -> {
                    AsyncImage(
                        model = content.imageUrl,
                        contentDescription = content.contentDescription,
                        modifier = Modifier
                            .size(40.dp)
                            .clip(CircleShape),
                        contentScale = ContentScale.Crop
                    )
                }
            }
            Text(
                text = message.timestamp,
                fontSize = 12.sp
            )
        }
    }
}

enter image description here

Any idea what I'm doing wrong and why the Surface doesn't fit the size of the text?


Solution

  • This is a bug / not supported feature that is present since the beginnings of Jetpack Compose, as described in Issue #206039942 on the Google Issue Tracker.

    There is a suggested workaround that you can use. The Text Composable has a onTextLayout callback which returns a TextLayoutResult. The TextLayoutResult holds information about the coordinates of each individual line displayed in the Text Composable.

    By then applying a layout Modifier, you can alter the size of the Text Composable to match the measured width of the longest line inside of the Text Composable.

    You can create a WrappingText Composable like this:

    @Composable
    fun WrappingText(
        text: String,
        modifier: Modifier = Modifier,
        color: Color = Color.Unspecified,
        fontSize: TextUnit = TextUnit.Unspecified,
        fontStyle: FontStyle? = null,
        fontWeight: FontWeight? = null,
        fontFamily: FontFamily? = null,
        letterSpacing: TextUnit = TextUnit.Unspecified,
        textDecoration: TextDecoration? = null,
        textAlign: TextAlign? = null,
        lineHeight: TextUnit = TextUnit.Unspecified,
        overflow: TextOverflow = TextOverflow.Clip,
        softWrap: Boolean = true,
        maxLines: Int = Int.MAX_VALUE,
        minLines: Int = 1,
        style: TextStyle = LocalTextStyle.current
    
    ) {
        var textLayoutResult: TextLayoutResult? by remember { mutableStateOf(null) }
        Text(
            text = text,
            modifier = modifier
                .layout { measurable, constraints ->
                    val placeable = measurable.measure(constraints)
                    val newTextLayoutResult = textLayoutResult!!
    
                    if (newTextLayoutResult.lineCount == 0) {
                        // Default behavior if there is no text
                        layout(placeable.width, placeable.height) {
                            placeable.placeRelative(0, 0)
                        }
                    } else {
                        // get coordinate of line which goes the furthest to the left
                        val minX = (0 until newTextLayoutResult.lineCount).minOf(newTextLayoutResult::getLineLeft)
                        // get coordinate of line which goes the furthest to the right
                        val maxX = (0 until newTextLayoutResult.lineCount).maxOf(newTextLayoutResult::getLineRight)
                        // set width to match longest line
                        layout(ceil(maxX - minX).toInt(), placeable.height) {
                            placeable.placeRelative(-floor(minX).toInt(), 0)
                        }
                    }
                },
            onTextLayout = {
                textLayoutResult = it
            },
            color = color,
            fontSize = fontSize,
            fontStyle = fontStyle,
            fontWeight = fontWeight,
            fontFamily = fontFamily,
            letterSpacing = letterSpacing,
            textDecoration = textDecoration,
            textAlign = textAlign,
            lineHeight = lineHeight,
            overflow = overflow,
            softWrap = softWrap,
            maxLines = maxLines,
            minLines = minLines,
            style = style
        )
    }
    

    Then, use it in your MessageItem Composable like this:

    Surface(
        modifier = Modifier.wrapContentSize(),
        shape = RoundedCornerShape(8.dp),
        color = MaterialTheme.colorScheme.primary
    ) {
        WrappingText(
            modifier = Modifier
                .padding(8.dp)
                .wrapContentSize(),
            text = "Are you going to that Kotlin conference to Colorado next week, my dear friend?",
            color = MaterialTheme.colorScheme.onPrimary,
            textAlign = TextAlign.End
        )
    }
    

    Output:

    Screenshot