Jetpack Compose에서의 Bullet Text

Jetpack Compose에서의 Bullet Text

Oct 11, 2025. | By: pluulove

AndroidX Compose UI 1.8.0에서 Bullet을 지원하기 시작했습니다.

androidx.compose.ui 1.8.0

기존 View 시스템에서의 Bullet 텍스트 적용 방법을 소개한 글이 있으니 관심 있는 분은 읽어보시길 바랍니다.

본 글에서는 다루지 않지만, Jetpack Compose도 내부적으로 비슷한 Span을 사용합니다.

기본 샘플

Jetpack Compose에서 Bullet 사용 시에는 AnnotatedString 생성에 사용하는 buildAnnotatedString Builder를 사용합니다.

// 위 샘플에 해당하는 코드
Text(
   text = buildAnnotatedString {
      withBulletList {
         withBulletListItem {
            append("Text")
         }
      }
   }
)

withBulletList

먼저 withBulletList API를 사용해서 Bullet에 대한 공통 들여쓰기 및 Bullet 기호를 정의할 수 있는 DSL을 만듭니다.

  • indentation : 들여쓰기 수치 (기본값 : 1em)
  • bullet : Bullet 모양 (기본값 : CircleShape, 0.25.em 크기, 0.25.em 여백)
fun <R : Any> withBulletList(
   indentation: TextUnit = Bullet.DefaultIndentation,
   bullet: Bullet = Bullet.Default,
   block: AnnotatedString.Builder.BulletScope.() -> R
): R

withBulletListItem

withBulletListItem API는 내부 블록에서 정의한 콘텐츠 주위에 Bullet 기호를 생성합니다. 기본적으로 이전 Builder.withBulletList API에서 정의된 Bullet 기호와 들여쓰기를 적용하여 별도의 단락을 생성합니다.

fun <R : Any> AnnotatedString.Builder.BulletScope.withBulletListItem(
   bullet: Bullet? = null,
   block: AnnotatedString.Builder.() -> R
): R

실제로 withBulletList 내부 block에서 withBulletListItem API 호출없이 append만 한다면, indentation(들여쓰기)만 적용됩니다. Bullet 사용에는 withBulletListItem API가 무조건 사용된다고 생각하면 됩니다.

Text(
   text = buildAnnotatedString {
      withBulletList {
         append("ABCD") // Bullet 미노출
         withBulletListItem {
            append("ABCD") // Bullet 노출
         }
      }
   }
)

복수 텍스트 크기 테스트

아래 샘플 코드에 fontSize에 10sp ~ 20sp를 적용하면, 기본 Bullet이더라도 폰트 크기에 맞춰서 Bullet의 크기와 여백이 적용됩니다.

@Composable
fun TextSample(
   fontSize: TextUnit = LocalTextStyle.current.fontSize
) {
   Text(
      text = buildAnnotatedString {
         withBulletList {
            withBulletListItem {
               append("Text")
            }
         }
      },
      fontSize = fontSize,
      ...
   )
}

Bullet의 기본 값들은 em이라는 Text Unit을 사용하므로 텍스트 크기에 상대적으로 맞춰집니다.

  • Em에 대해서 : https://fonts.google.com/knowledge/glossary/em
class Bullet(
   ...
) : AnnotatedString.Annotation {
   companion object {
      /** Indentation required to fit [Default] bullet. */
      val DefaultIndentation = 1.em

      /** Height and width for [Default] bullet. */
      val DefaultSize = 0.25.em

      /** Padding between bullet and start of paragraph for [Default] bullet */
      val DefaultPadding = 0.25.em

      /** Default bullet used in AnnotatedString's bullet list */
      val Default = Bullet(CircleShape, DefaultSize, DefaultSize, DefaultPadding)
   }
}

Bullet 소스 출처 : 링크

HTML 태그가 있는 문자열에서 Bullet 확인

HTML 태그가 있는 String의 경우에는 AnnotatedString.fromHtml을 사용하면 쉽게 AnnotatedString을 얻을 수 있습니다. AnnotatedString을 지원하는 Text/BasicText에 전달하면 Bullet이 노출됩니다.

Text(
   text = AnnotatedString.fromHtml(
      "<ul><li>가나다라<ul><li>ABCD</li></ul></li><ul>"
   )
)

추가로 기본 Bullet 스타일을 사용한 withBulletList과 비교하면 결과는 동일한 것을 볼 수 있습니다.

Column(...) {
   // withBulletList 샘플
   Text(
      text = buildAnnotatedString {
         withBulletList {
            withBulletListItem {
               append("가나다라")
               withBulletList {
                  withBulletListItem {
                     append("ABCD")
                  }
               }
            }
         }
      }
   )

   // AnnotatedString.fromHtml을 사용한 샘플
   Text(
      text = AnnotatedString.fromHtml(
         "<ul><li>가나다라<ul><li>ABCD</li><ul></li><ul>"
      )
   )
}

buildAnnotatedString과 fromHtml 비교

AnnotatedString의 DSL 패턴인 buildAnnotatedString과 AnnotatedString.fromHtml 코드는 작성 방법은 다르지만, 최종적으로 AnnotatedString을 만드는 Spanned.toAnnotatedString 확장 함수를 호출하고 있습니다.

// buildAnnotatedString Builder
inline fun buildAnnotatedString(builder: (Builder).() -> Unit): AnnotatedString =
    Builder().apply(builder).toAnnotatedString()

// AnnotatedString.fromHtml 확장 함수
fun AnnotatedString.Companion.fromHtml(
   ...
): AnnotatedString {
   ...
   val spanned =
      HtmlCompat.fromHtml(stringToParse, HtmlCompat.FROM_HTML_MODE_COMPACT, null, TagHandler)
   return spanned.toAnnotatedString(linkStyles, linkInteractionListener)
}
  • buildAnnotatedString 소스 출처 : 링크
  • fromHtml 소스 출처 : 링크

Spanned.toAnnotatedString 확장 함수는 internal 접근 제한자로 정의되어 있습니다.

@VisibleForTesting
internal fun Spanned.toAnnotatedString(
   linkStyles: TextLinkStyles? = null,
   linkInteractionListener: LinkInteractionListener? = null,
): AnnotatedString {
   return AnnotatedString.Builder(capacity = length)
      .append(this)
      .also { it.addSpans(this, linkStyles, linkInteractionListener) }
      .toAnnotatedString()
}

Spanned.toAnnotatedString 소스 출처 : 링크

추가 샘플

중첩 기본 Bullet

Text(
   text = buildAnnotatedString {
      withBulletList {
         withBulletListItem { append("Item 1") }
         withBulletList {
            withBulletListItem { append("Item 2") }
            withBulletListItem { append("Item 3") }
            withBulletList {
               withBulletListItem { append("Item 4") }
            }
         }
         withBulletListItem { append("Item 5") }
      }
   }
)

중첩 커스텀 스타일 Bullet

  • bullet1 : 클로버 모양 + 기본 크기의 2배
  • bullet2 : bullet1 + Stroke 형태
  • bullet3 : bullet1 + Gradient Brush

val bullet1 = Bullet.Default.copy(
   shape = MaterialShapes.Clover4Leaf.toShape(),
   width = Bullet.DefaultSize * 2,
   height = Bullet.DefaultSize * 2,
)
val bullet2 = bullet1.copy(drawStyle = Stroke(2f))
val bullet3 = bullet1.copy(
   brush = Brush.horizontalGradient(
      0.0f to Color.Red,
      0.3f to Color.Green,
      1.0f to Color.Blue
   )
)
Text(
   text = buildAnnotatedString {
      withBulletList(bullet = bullet1) {
         withBulletListItem { append("Item 1") }
         withBulletList(bullet = bullet2) {
            withBulletListItem { append("Item 2") }
            withBulletListItem { append("Item 3") }
            withBulletList(bullet = bullet3) {
               withBulletListItem { append("Item 4") }
            }
         }
         withBulletListItem { append("Item 5") }
      }
   }
)

Currnte Pages Tags

Android AndroidX

About

Pluu, Android Developer Blog Site

이전 블로그 링크 :네이버 블로그

Using Theme : SOLID SOLID Github

Social Links