CodeWithVikas
CodeWithVikas

Reputation: 1443

How to create bulleted text list in Android Jetpack Compose?

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.

Upvotes: 25

Views: 14177

Answers (5)

Inidam Leader
Inidam Leader

Reputation: 629

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

Upvotes: 0

Xam
Xam

Reputation: 351

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))

Upvotes: 6

juhopekka
juhopekka

Reputation: 71

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

Upvotes: 7

CodeWithVikas
CodeWithVikas

Reputation: 1443

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

Upvotes: 32

Yshh
Yshh

Reputation: 832

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)
            }
        }
    }
}

Upvotes: 10

Related Questions