How to create bulleted text list in Android Jetpack Compose?
Asked Answered
C

5

22

I want something like this:

• Hey this is my first paragraph.
• Hey this is my second paragraph.
  And this is the second line.
• Hey this is my third paragraph.
Comprise answered 15/1, 2022 at 18:23 Comment(0)
C
32

Found it while brainstorming. Just another approach with annotated string and only one Text.

val bullet = "\u2022"
    val messages = listOf(
        "Hey This is first paragraph",
        "Hey this is my second paragraph. Any this is 2nd line.",
        "Hey this is 3rd paragraph."
    )
    val paragraphStyle = ParagraphStyle(textIndent = TextIndent(restLine = 12.sp))
    Text(
        buildAnnotatedString {
            messages.forEach {
                withStyle(style = paragraphStyle) {
                    append(bullet)
                    append("\t\t")
                    append(it)
                }
            }
        }
    )

update: screenshot of output enter image description here

Comprise answered 21/1, 2022 at 11:21 Comment(7)
How does this render in compose? Do you have a screenshot of what this produces? I see, you are just using the character code for bullet and appending it.Dannettedanni
sure. I have updated my answer.Comprise
Unfortunately it's not working on all phones (the 12 pixels restline will not be aligned with the two tabs depending on the screen resolution)Touraco
What will happen on long multiline text?Concur
It should work for multiline in similar way. But any better approach will be welcomed.Comprise
Thank you @Comprise it works with hard code text. Do have any suggestion/reference to make it work from string resources ? <li>, <ol> & <ul>Concur
See Xam's answer for a solution that is very similar to this answer but which measures the restLine value rather than using a hardcoded value. The problem with using a hardcoded value, as Arpit Patel has pointed out above, is that it may not work so perfectly on all phones.Mitchellmitchem
F
8

I don't know if it can meet expectations,please try

@Preview(showBackground = true)
@Composable
fun TestList() {
    val list = listOf(
        "Hey This is first paragraph",
        "Hey this is my second paragraph. Any this is 2nd line.",
        "Hey this is 3rd paragraph."
    )
    LazyColumn {
        items(list) {
            Row(Modifier.padding(8.dp),verticalAlignment = Alignment.CenterVertically) {
                Canvas(modifier = Modifier.padding(start = 8.dp,end = 8.dp).size(6.dp)){
                    drawCircle(Color.Black)
                }
                Text(text = it,fontSize = 12.sp)
            }
        }
    }
}
Fritzie answered 17/1, 2022 at 1:26 Comment(1)
Good one.However i was expecting something with BulletSpan in compose. But this is also a good solution thanks.Comprise
S
5

Just composed this kind of component

@Composable
fun BulletList(
    modifier: Modifier = Modifier,
    style: TextStyle,
    indent: Dp = 20.dp,
    lineSpacing: Dp = 0.dp,
    items: List<String>,
) {
    Column(modifier = modifier) {
        items.forEach {
            Row {
                Text(
                    text = "\u2022",
                    style = style.copy(textAlign = TextAlign.Center),
                    modifier = Modifier.width(indent),
                )
                Text(
                    text = it,
                    style = style,
                    modifier = Modifier.weight(1f, fill = true),
                )
            }
            if (lineSpacing > 0.dp && it != items.last()) {
                Spacer(modifier = Modifier.height(lineSpacing))
            }
        }
    }
}

Usage

BulletList(
    items = listOf(
        "First bullet",
        "Second bullet ... which is awfully long but that's not a problem",
        "Third bullet ",
    ),
    modifier = Modifier.padding(24.dp),
    style = MyTheme.typography.body1,
    lineSpacing = 8.dp,
)

Result

Snowplow answered 21/6, 2023 at 9:37 Comment(0)
K
4

This answer is based on the accepted answer. I hope it is not redundant. I think it fixes the issue about hardcoding the size of the restLine by measuring the size of the two tabs instead. We also made it more generic.

@Composable
fun makeBulletedList(items: List<String>): AnnotatedString {
    val bulletString = "\u2022\t\t"
    val textStyle = LocalTextStyle.current
    val textMeasurer = rememberTextMeasurer()
    val bulletStringWidth = remember(textStyle, textMeasurer) {
        textMeasurer.measure(text = bulletString, style = textStyle).size.width
    }
    val restLine = with(LocalDensity.current) { bulletStringWidth.toSp() }
    val paragraphStyle = ParagraphStyle(textIndent = TextIndent(restLine = restLine))

    return buildAnnotatedString {
        items.forEach { text ->
            withStyle(style = paragraphStyle) {
                append(bulletString)
                append(text)
            }
        }
    }
}

You can use this function like this:

items = listOf(
    "First item",
    "Second item is too long to fit in one line, but that's not a problem",
    "Third item",
)

Text(text = makeBulletedList(items))
Keil answered 6/12, 2023 at 16:15 Comment(3)
This is great! Just one thing: You should measure the width of the bullet character together with the width of the two tabs. Without this the text that spans over onto subsequent lines is indented a little to the left of the first line. I'm happy to edit your answer to include this change. Just let me know.Mitchellmitchem
@AdilHussain oh yeah, you're right. Feel free to edit my answer :).Keil
Done. I joined up the bullet and tab characters into a bulletString value.Mitchellmitchem
M
0

Calculate the correct restLine value

UnorderedListText.kt:

package com.inidamleader.ovtracker.util.compose

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.inidamleader.ovtracker.layer.ui.theme.OvTrackerTheme
import com.inidamleader.ovtracker.util.compose.geometry.toSp

@Composable
fun UnorderedListText(
    text: String,
    modifier: Modifier = Modifier,
    bullet: String = "•  ",
    highlightedText: String = "",
    highlightedTextColor: Color = MaterialTheme.colorScheme.primaryContainer,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    minLines: Int = 1,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current,
) {
    val restLine = run {
        val textMeasurer = rememberTextMeasurer()
        remember(key1 = bullet, key2 = style) {
            textMeasurer.measure(text = bullet, style = style).size.width
        }.toSp
    }
    Text(
        text = remember(
            text,
            bullet,
            restLine,
            highlightedText,
            highlightedTextColor,
        ) {
            text.unorderedListAnnotatedString(
                bullet = bullet,
                restLine = restLine,
                highlightedText = highlightedText,
                highlightedTxtColor = highlightedTextColor,
            )
        },
        overflow = overflow,
        softWrap = softWrap,
        maxLines = maxLines,
        minLines = minLines,
        onTextLayout = onTextLayout,
        style = style,
        modifier = modifier,
    )
}

@Preview
@Composable
fun PreviewUnorderedListText() {
    OvTrackerTheme {
        Surface {
            UnorderedListText(
                text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." +
                        "\nEtiam lipsums et metus vel mauris scelerisque molestie eget nec ligula." +
                        "\nNulla scelerisque, magna id aliquam rhoncus, ipsumx turpis risus sodales mi, sit ipsum amet malesuada nibh lacus sit amet libero." +
                        "\nCras in sem euismod, vulputate ligula in, egestas enim ipsum.",
                modifier = Modifier.padding(8.dp),
                highlightedText = "ipsum",
                overflow = TextOverflow.Ellipsis,
                onTextLayout = {},
            )
        }
    }
}

StringExt.kt:

package com.inidamleader.ovtracker.util.compose

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp

fun String.unorderedListAnnotatedString(
    bullet: String = "•  ",
    restLine: TextUnit = 12.sp,
    highlightedText: String = "",
    highlightedTxtColor: Color = Color.Cyan,
) = buildAnnotatedString {
    split("\n").forEach {
        var txt = it.trim()
        if (txt.isNotBlank()) {
            withStyle(style = ParagraphStyle(textIndent = TextIndent(restLine = restLine))) {
                append(bullet)
                if (highlightedText.isNotEmpty()) {
                    while (true) {
                        val i = txt.indexOf(string = highlightedText, ignoreCase = true)
                        if (i == -1) break
                        append(txt.subSequence(startIndex = 0, endIndex = i).toString())
                        val j = i + highlightedText.length
                        withStyle(style = SpanStyle(background = highlightedTxtColor)) {
                            append(txt.subSequence(startIndex = i, endIndex = j).toString())
                        }
                        txt = txt.subSequence(startIndex = j, endIndex = txt.length).toString()
                    }
                }
                append(txt)
            }
        }
    }
}

Preview

Macswan answered 31/12, 2023 at 17:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.