Как стилизовать текст на Compose через Spannable

от автора

Всем привет от Андроид-команды банка ДОМ.РФ! Сегодня поговорим о стилизации. Иногда нужно добавить возможность стилизовать разные части текста. Как правило, для этого используют AnnotatedString. Мы можем форматировать текст как просто кусками, так и задавать определенные позиции, подсвечивать ссылки, обрабатывать клики на них. Этот подход замечательно работает, когда мы имеем готовый текст и заранее знаем, где и каким образом мы будем стилизовать. Но давайте попробуем рассмотреть две задачи:

  1. У нас есть блок текста, который откуда-то приходит, и чтобы поддержать множество возможных комбинаций стилей, текст приходит как html разметка.

  2. С бэкенда приходит совершенно рандомный текст, в котором нам нужно отыскать ссылку и стилизовать ее.

Что может прийти в голову? Написать свою реализацию для форматирования текста. Но только представьте, сколько всего нужно учесть. При этом никто не избавит нас от фикса багов. Или же пойти другим путем и попробовать найти готовое решение и немного его доработать под Compose.

Html текст и Spannable

val htmlText = "<b>Для просмотра нового о compose</b> \n<a href=\"https://developer.android.com/jetpack/androidx/releases/compose\" target=\"_blank\">" + "нажмите на этот текст</a>, там много интересного" // Spanned съедает спецсимволы \n, поэтому заменяем его на тег переноса <br> val formattedText = Html.fromHtml(htmlText.replace("\n", "<br>"), Html.FROM_HTML_MODE_COMPACT)

Html.fromHtml возвращает искомый нами Spanned. Пару слов стоит сказать, что такое Spannable. Это довольно старая концепция, которая широко используется для стилизации текста. TextView умеет напрямую работать с ним в отличие от public api jetpack Compose. Теперь нам нужно конвертировать наши Spans стили в то, что съест Compose. Для этого нам понадобится AnnotatedString.

const val URL_TAG = "url"  fun Spanned.toAnnotateString(     baseSpanStyle: SpanStyle?,     linkColor: Color ): AnnotatedString {     return buildAnnotatedString {         val spanned = this@toAnnotateString         append(spanned.toString())         baseSpanStyle?.let { addStyle(it, 0, length) }         getSpans(0, spanned.length, Any::class.java).forEach { span ->             val start = getSpanStart(span)             val end = getSpanEnd(span)             when (span) {                 is StyleSpan -> when (span.style) {                     Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end)                     Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end)                     Typeface.BOLD_ITALIC -> addStyle(                         SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic),                         start,                         end                     )                 }                 is UnderlineSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)                 is ForegroundColorSpan -> addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end)                 is URLSpan -> {                     addStyle(                         SpanStyle(                             textDecoration = TextDecoration.Underline,                             color = linkColor                         ), start, end                     )                     addStringAnnotation(URL_TAG, span.url, start, end)                 }             }         }     } }

Для удобства оформил в экстеншен Spanned.toAnnotateString. Задаем базовый стиль для всего текста. Если нужно — и дальше перебираем Spans, стилизуя текст c помощью addStyle. Можно заметить URLSpan, он в дальнейшем нам еще пригодится. Тех спанов, что есть в экстеншене, хватает на нашем проекте. Вы же можете поиграться сами, придумав стили для других. Теперь посмотрим всю реализацию вместе с обработкой нажатий на ссылки, это уже стандартный подход на Compose.

@Composable fun HtmlTextField(     modifier: Modifier = Modifier,     baseSpanStyle: SpanStyle? = null,     isHighlightLink: Boolean = false,     style: TextStyle = LocalTextStyle.current,     onUrlClick: ((url: String) -> Unit)? = null ) {     val htmlText = "<b>Для просмотра нового о compose</b> \n<a href=\"https://developer.android.com/jetpack/androidx/releases/compose\" target=\"_blank\">" +     "нажмите на этот текст</a>, там много интересного"     val formattedText = Html.fromHtml(htmlText.replace("\n", "<br>"), Html.FROM_HTML_MODE_COMPACT)     val uriHandler = LocalUriHandler.current     val linkColor = if (isHighlightLink) Color.Blue else Color.Unspecified     val annotatedString = formattedText.toAnnotateString(baseSpanStyle = baseSpanStyle, linkColor = linkColor)     ClickableText(         modifier = modifier,         text = annotatedString,         style = style,     ) { offset ->         annotatedString.getStringAnnotations(URL_TAG, offset, offset).firstOrNull()?.let {             onUrlClick?.let { click -> click(it.item) } ?: uriHandler.openUri(it.item)         }     } }
Так выглядит стилизованный html в Compose

Так выглядит стилизованный html в Compose

ClickableText мы используем для того чтобы найти ссылку и обработать нажатие. Выглядит хорошо, теперь перейдем ко второй задаче.

Нам поможет Linkify

У TextView есть замечательный атрибут autoLink, который за нас ищет и хайлайтит ссылки, а также обрабатывает клики по ним. Если покопаться, то он использует Linkify, в этом классе нас интересует метод addLinks, он принимает Span на вход и вторым параметром — тип маски.

@Composable fun Message() {     val someText = "Хочу открыть https://developer.android.com, что нового?"     val uriHandler = LocalUriHandler.current     val spannedText = someText.toSpannable() // превращаем в Spannable, так как Linkify работает со Spannable     Linkify.addLinks(spannedText, Linkify.WEB_URLS) // Ищем и размечаем URLSpan     val annotatedString = spannedText.toAnnotateString(         baseSpanStyle = SpanStyle(             color = Color.Black,         ),         linkColor = Color.Blue     )     ClickableText(         modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),         text = annotatedString,         style = MaterialTheme.typography.bodyLarge,         onClick = { offset ->             annotatedString.getStringAnnotations(URL_TAG, offset, offset).firstOrNull()?.let {                 uriHandler.openUri(it.item)             }         }     ) }
Стилизованный текст с кликабельной ссылкой

Стилизованный текст с кликабельной ссылкой

Заключение

При помощи Spannable и Linkify мы можем облегчить себе задачи по форматированию любого текст на лету и добавлять стили и из своей дизайн-системы. Возможно, вы как-то по-другому решали эту задачу, пишите в комментариях, будет очень интересно почитать.


ссылка на оригинал статьи https://habr.com/ru/articles/770796/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *