Всем привет от Андроид-команды банка ДОМ.РФ! Сегодня поговорим о стилизации. Иногда нужно добавить возможность стилизовать разные части текста. Как правило, для этого используют AnnotatedString. Мы можем форматировать текст как просто кусками, так и задавать определенные позиции, подсвечивать ссылки, обрабатывать клики на них. Этот подход замечательно работает, когда мы имеем готовый текст и заранее знаем, где и каким образом мы будем стилизовать. Но давайте попробуем рассмотреть две задачи:
-
У нас есть блок текста, который откуда-то приходит, и чтобы поддержать множество возможных комбинаций стилей, текст приходит как html разметка.
-
С бэкенда приходит совершенно рандомный текст, в котором нам нужно отыскать ссылку и стилизовать ее.
Что может прийти в голову? Написать свою реализацию для форматирования текста. Но только представьте, сколько всего нужно учесть. При этом никто не избавит нас от фикса багов. Или же пойти другим путем и попробовать найти готовое решение и немного его доработать под 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) } } }
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/
Добавить комментарий