Jetpack Compose Text гиперссылка на некоторый фрагмент текста
Как добавить гиперссылку на какой-либо раздел текста текстового компонента?
С
buildAnnotatedString
Я могу сделать раздел ссылки синим и подчеркнутым, как на изображении ниже, но как я могу также превратить этот раздел в ссылку?
val annotatedLinkString = buildAnnotatedString {
val str = "Click this link to go to web site"
val startIndex = str.indexOf("link")
val endIndex = startIndex + 4
append(str)
addStyle(
style = SpanStyle(
color = Color(0xff64B5F6),
textDecoration = TextDecoration.Underline
), start = startIndex, end = endIndex
)
}
Text(
modifier = modifier
.padding(16.dp)
.fillMaxWidth(),
text = annotatedLinkString
)
Я также могу получить
Spanned
но есть ли способ использовать его с
Text
?
val str: Spanned = HtmlCompat.fromHtml(
"<a href=\"http://www.github.com\">Github</a>", HtmlCompat.FROM_HTML_MODE_LEGACY
)
12 ответов
Как добавить гиперссылку на какой-либо раздел текста текстового компонента?
with(AnnotatedString.Builder()) {
append("link: Jetpack Compose")
// attach a string annotation that stores a URL to the text "Jetpack Compose".
addStringAnnotation(
tag = "URL",
annotation = "https://developer.android.com/jetpack/compose",
start = 6,
end = 21
)
}
tag : Тег, используемый для различения аннотаций.
annotation : строковая аннотация, прикрепленная
start : включающее начальное смещение диапазона
end : исключительное конечное смещение
Отмеченный ответ смущает новичков, привожу полный пример
val annotatedString = buildAnnotatedString {
append("By joining, you agree to the ")
pushStringAnnotation(tag = "policy", annotation = "https://google.com/policy")
withStyle(style = SpanStyle(color = MaterialTheme.colors.primary)) {
append("privacy policy")
}
append(" and ")
pushStringAnnotation(tag = "terms", annotation = "https://google.com/terms")
withStyle(style = SpanStyle(color = MaterialTheme.colors.primary)) {
append("terms of use")
}
pop()
}
ClickableText(text = annotatedString, style = MaterialTheme.typography.body1, onClick = { offset ->
annotatedString.getStringAnnotations(tag = "policy", start = offset, end = offset).firstOrNull()?.let {
Log.d("policy URL", it.item)
}
annotatedString.getStringAnnotations(tag = "terms", start = offset, end = offset).firstOrNull()?.let {
Log.d("terms URL", it.item)
}
})
конечный эффект
Для полного ответа вы можете использовать
ClickableText
который возвращает позицию текста, и
UriHandler
чтобы открыть URI в браузере.
val annotatedLinkString: AnnotatedString = buildAnnotatedString {
val str = "Click this link to go to web site"
val startIndex = str.indexOf("link")
val endIndex = startIndex + 4
append(str)
addStyle(
style = SpanStyle(
color = Color(0xff64B5F6),
fontSize = 18.sp,
textDecoration = TextDecoration.Underline
), start = startIndex, end = endIndex
)
// attach a string annotation that stores a URL to the text "link"
addStringAnnotation(
tag = "URL",
annotation = "https://github.com",
start = startIndex,
end = endIndex
)
}
// UriHandler parse and opens URI inside AnnotatedString Item in Browse
val uriHandler = AmbientUriHandler.current
// 🔥 Clickable text returns position of text that is clicked in onClick callback
ClickableText(
modifier = modifier
.padding(16.dp)
.fillMaxWidth(),
text = annotatedLinkString,
onClick = {
annotatedLinkString
.getStringAnnotations("URL", it, it)
.firstOrNull()?.let { stringAnnotation ->
uriHandler.openUri(stringAnnotation.item)
}
}
)
Для тех, кто ищет многоразовое решение для копирования и вставки,
Создать новый файл
LinkText.kt
и скопируйте и вставьте этот код,
data class LinkTextData(
val text: String,
val tag: String? = null,
val annotation: String? = null,
val onClick: ((str: AnnotatedString.Range<String>) -> Unit)? = null,
)
@Composable
fun LinkText(
linkTextData: List<LinkTextData>,
modifier: Modifier = Modifier,
) {
val annotatedString = createAnnotatedString(linkTextData)
ClickableText(
text = annotatedString,
style = MaterialTheme.typography.body1,
onClick = { offset ->
linkTextData.forEach { annotatedStringData ->
if (annotatedStringData.tag != null && annotatedStringData.annotation != null) {
annotatedString.getStringAnnotations(
tag = annotatedStringData.tag,
start = offset,
end = offset,
).firstOrNull()?.let {
annotatedStringData.onClick?.invoke(it)
}
}
}
},
modifier = modifier,
)
}
@Composable
private fun createAnnotatedString(data: List<LinkTextData>): AnnotatedString {
return buildAnnotatedString {
data.forEach { linkTextData ->
if (linkTextData.tag != null && linkTextData.annotation != null) {
pushStringAnnotation(
tag = linkTextData.tag,
annotation = linkTextData.annotation,
)
withStyle(
style = SpanStyle(
color = MaterialTheme.colors.primary,
textDecoration = TextDecoration.Underline,
),
) {
append(linkTextData.text)
}
pop()
} else {
append(linkTextData.text)
}
}
}
}
использование
LinkText(
linkTextData = listOf(
LinkTextData(
text = "Icons made by ",
),
LinkTextData(
text = "smalllikeart",
tag = "icon_1_author",
annotation = "https://www.flaticon.com/authors/smalllikeart",
onClick = {
Log.d("Link text", "${it.tag} ${it.item}")
},
),
LinkTextData(
text = " from ",
),
LinkTextData(
text = "Flaticon",
tag = "icon_1_source",
annotation = "https://www.flaticon.com/",
onClick = {
Log.d("Link text", "${it.tag} ${it.item}")
},
)
),
modifier = Modifier
.padding(
all = 16.dp,
),
)
Скриншот ,
Примечание
- Я обрабатываю веб-страницы вручную, используя компонент. Использовать
UriHandler
или другие альтернативы, если ручное управление не требуется. - Стиль интерактивного и другого текста, как требуется в
LinkText
.
Вы можете использовать https://github.com/firefinchdev/linkify-text
Это один файл, вы можете напрямую скопировать его в свой проект.
Кроме того, он использует Android Linkify для обнаружения ссылок, который такой же, как у
TextView
х
autoLink
.
Самое простое и чистое решение:
@Composable
fun AnnotatedClickableText() {
val termsUrl = "https://example.com/terms"
val privacyUrl = "https://example.com/privacy"
val annotatedText = buildAnnotatedString {
append("You agree to our ")
withStyle(style = SpanStyle(color = Color.Blue, fontWeight = FontWeight.Bold)) {
appendLink("Terms of Use", termsUrl)
}
append(" and ")
withStyle(style = SpanStyle(color = Color.Blue, fontWeight = FontWeight.Bold)) {
appendLink("Privacy Policy", privacyUrl)
}
}
ClickableText(
text = annotatedText,
onClick = { offset ->
annotatedText.onLinkClick(offset) { link ->
println("Clicked URL: $link")
// Open link in WebView.
}
}
)
}
fun AnnotatedString.Builder.appendLink(linkText: String, linkUrl: String) {
pushStringAnnotation(tag = linkUrl, annotation = linkUrl)
append(linkText)
pop()
}
fun AnnotatedString.onLinkClick(offset: Int, onClick: (String) -> Unit) {
getStringAnnotations(start = offset, end = offset).firstOrNull()?.let {
onClick(it.item)
}
}
Обратите внимание на 2 функции расширения, которые значительно упрощают создание ссылок.
Если вы хотите использовать @StringRes изstrings.xml
файл, вы можете использовать код ниже
Допустим, у вас есть следующие строковые ресурсы:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="disclaimer">By joining you agree to the privacy policy and terms of use.</string>
<string name="privacy_policy">privacy policy</string>
<string name="terms_of_use">terms of use</string>
</resources>
Вы можете использовать его следующим образом:
HighlightedText(
text = stringResource(id = R.string.disclaimer),
highlights = listOf(
Highlight(
text = stringResource(id = R.string.privacy_policy),
data = "https://stackoverflow.com/legal/privacy-policy",
onClick = { link ->
// do something with link
}
),
Highlight(
text = stringResource(id = R.string.terms_of_use),
data = "https://stackoverflow.com/legal/terms-of-use",
onClick = { link ->
// do something with link
}
)
)
)
Вот исходный код Composable:
data class Highlight(
val text: String,
val data: String,
val onClick: (data: String) -> Unit
)
@Composable
fun HighlightedText(
text: String,
highlights: List<Highlight>,
modifier: Modifier = Modifier
) {
data class TextData(
val text: String,
val tag: String? = null,
val data: String? = null,
val onClick: ((data: AnnotatedString.Range<String>) -> Unit)? = null
)
val textData = mutableListOf<TextData>()
if (highlights.isEmpty()) {
textData.add(
TextData(
text = text
)
)
} else {
var startIndex = 0
highlights.forEachIndexed { i, link ->
val endIndex = text.indexOf(link.text)
if (endIndex == -1) {
throw Exception("Highlighted text mismatch")
}
textData.add(
TextData(
text = text.substring(startIndex, endIndex)
)
)
textData.add(
TextData(
text = link.text,
tag = "${link.text}_TAG",
data = link.data,
onClick = {
link.onClick(it.item)
}
)
)
startIndex = endIndex + link.text.length
if (i == highlights.lastIndex && startIndex < text.length) {
textData.add(
TextData(
text = text.substring(startIndex, text.length)
)
)
}
}
}
val annotatedString = buildAnnotatedString {
textData.forEach { linkTextData ->
if (linkTextData.tag != null && linkTextData.data != null) {
pushStringAnnotation(
tag = linkTextData.tag,
annotation = linkTextData.data,
)
withStyle(
style = SpanStyle(
color = infoLinkTextColor
),
) {
append(linkTextData.text)
}
pop()
} else {
append(linkTextData.text)
}
}
}
ClickableText(
text = annotatedString,
style = TextStyle(
fontSize = 30.sp,
fontWeight = FontWeight.Normal,
color = infoTextColor,
textAlign = TextAlign.Start
),
onClick = { offset ->
textData.forEach { annotatedStringData ->
if (annotatedStringData.tag != null && annotatedStringData.data != null) {
annotatedString.getStringAnnotations(
tag = annotatedStringData.tag,
start = offset,
end = offset,
).firstOrNull()?.let {
annotatedStringData.onClick?.invoke(it)
}
}
}
},
modifier = modifier
)
}
Все ответы здесь хороши, если вы используете жестко закодированные строки, но они не очень полезны для строковых ресурсов. Вот некоторый код, который дает вам функциональность, аналогичную тому, как TextViews старой школы будут работать с HTML, полностью созданным с использованием Jetpack Compose (без API-интерфейсов взаимодействия). Кредит на 99% этого ответа относится к комментарию по этой проблеме, который я расширил, чтобы использовать тег аннотации ресурса строки Android для поддержки URL-адресов. [Примечание: BulletSpan в настоящее время не поддерживается в этом решении, поскольку он не нужен для моего варианта использования, и я не тратил время на его отсутствие в расширенном решении]
const val URL_ANNOTATION_KEY = "url"
/**
* Much of this class comes from
* https://issuetracker.google.com/issues/139320238#comment11
* which seeks to correct the gap in Jetpack Compose wherein HTML style tags in string resources
* are not respected.
*/
@Composable
@ReadOnlyComposable
private fun resources(): Resources {
return LocalContext.current.resources
}
fun Spanned.toHtmlWithoutParagraphs(): String {
return HtmlCompat.toHtml(this, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
.substringAfter("<p dir=\"ltr\">").substringBeforeLast("</p>")
}
fun Resources.getText(@StringRes id: Int, vararg args: Any): CharSequence {
val escapedArgs = args.map {
if (it is Spanned) it.toHtmlWithoutParagraphs() else it
}.toTypedArray()
val resource = SpannedString(getText(id))
val htmlResource = resource.toHtmlWithoutParagraphs()
val formattedHtml = String.format(htmlResource, *escapedArgs)
return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
@Composable
fun annotatedStringResource(@StringRes id: Int, vararg formatArgs: Any): AnnotatedString {
val resources = resources()
val density = LocalDensity.current
return remember(id, formatArgs) {
val text = resources.getText(id, *formatArgs)
spannableStringToAnnotatedString(text, density)
}
}
@Composable
fun annotatedStringResource(@StringRes id: Int): AnnotatedString {
val resources = resources()
val density = LocalDensity.current
return remember(id) {
val text = resources.getText(id)
spannableStringToAnnotatedString(text, density)
}
}
private fun spannableStringToAnnotatedString(
text: CharSequence,
density: Density
): AnnotatedString {
return if (text is Spanned) {
with(density) {
buildAnnotatedString {
append((text.toString()))
text.getSpans(0, text.length, Any::class.java).forEach {
val start = text.getSpanStart(it)
val end = text.getSpanEnd(it)
when (it) {
is StyleSpan -> when (it.style) {
Typeface.NORMAL -> addStyle(
style = SpanStyle(
fontWeight = FontWeight.Normal,
fontStyle = FontStyle.Normal
),
start = start,
end = end
)
Typeface.BOLD -> addStyle(
style = SpanStyle(
fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Normal
),
start = start,
end = end
)
Typeface.ITALIC -> addStyle(
style = SpanStyle(
fontWeight = FontWeight.Normal,
fontStyle = FontStyle.Italic
),
start = start,
end = end
)
Typeface.BOLD_ITALIC -> addStyle(
style = SpanStyle(
fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Italic
),
start = start,
end = end
)
}
is TypefaceSpan -> addStyle(
style = SpanStyle(
fontFamily = when (it.family) {
FontFamily.SansSerif.name -> FontFamily.SansSerif
FontFamily.Serif.name -> FontFamily.Serif
FontFamily.Monospace.name -> FontFamily.Monospace
FontFamily.Cursive.name -> FontFamily.Cursive
else -> FontFamily.Default
}
),
start = start,
end = end
)
is BulletSpan -> {
Log.d("StringResources", "BulletSpan not supported yet")
addStyle(style = SpanStyle(), start = start, end = end)
}
is AbsoluteSizeSpan -> addStyle(
style = SpanStyle(fontSize = if (it.dip) it.size.dp.toSp() else it.size.toSp()),
start = start,
end = end
)
is RelativeSizeSpan -> addStyle(
style = SpanStyle(fontSize = it.sizeChange.em),
start = start,
end = end
)
is StrikethroughSpan -> addStyle(
style = SpanStyle(textDecoration = TextDecoration.LineThrough),
start = start,
end = end
)
is UnderlineSpan -> addStyle(
style = SpanStyle(textDecoration = TextDecoration.Underline),
start = start,
end = end
)
is SuperscriptSpan -> addStyle(
style = SpanStyle(baselineShift = BaselineShift.Superscript),
start = start,
end = end
)
is SubscriptSpan -> addStyle(
style = SpanStyle(baselineShift = BaselineShift.Subscript),
start = start,
end = end
)
is ForegroundColorSpan -> addStyle(
style = SpanStyle(color = Color(it.foregroundColor)),
start = start,
end = end
)
is Annotation -> {
if (it.key == URL_ANNOTATION_KEY) {
addStyle(
style = SpanStyle(color = Color.Blue),
start = start,
end = end
)
addStringAnnotation(
tag = "URL",
annotation = it.value,
start = start,
end = end
)
}
}
else -> addStyle(style = SpanStyle(), start = start, end = end)
}
}
}
}
} else {
AnnotatedString(text = text.toString())
}
}
@Composable
fun LinkableTextView(
@StringRes id: Int,
modifier: Modifier = Modifier,
style: TextStyle = MaterialTheme.typography.body1
) {
val uriHandler = LocalUriHandler.current
val annotatedString = annotatedStringResource(id)
ClickableText(
text = annotatedString,
style = style,
onClick = { offset ->
annotatedString.getStringAnnotations(
tag = "URL",
start = offset,
end = offset
).firstOrNull()?.let {
uriHandler.openUri(it.item)
}
},
modifier = modifier,
)
}
Применение:
@Composable
fun MyComposableView {
LinkableTextView(
id = R.string.my_link_string
)
}
Строковый ресурс:
<string name="my_link_string">Click this
<annotation url="https://www.stackoverflow.com">link</annotation>
to go to web site
</string>
Существует также «тупой» способ просто вернуться к использованию android.widget.TextView, который имеет поведение, которое вы ищете:
@Composable
fun CompatHtmlTextView(@StringRes htmlStringResource: Int) {
val html = stringResource(htmlStringResource)
AndroidView(factory = { context ->
android.widget.TextView(context).apply {
text = fromHtml(html)
}
})
}
Используйте этот код, если у вас нет руки для ввода текста.
val s = buildAnnotatedString {
for (link in txt.split(' ')) {
if (link.matches(".*(#\\w+)|(http(s)?://.+).*".toRegex())) {
withStyle(SpanStyle(color = Color.Cyan)) {
append(link + ' ')
}
} else {
append(link + ' ')
}
}
}
Text(text = s)
Это может быть больше#
иhttps://
это зависит от вас в регулярном выражении .
Примечание. Это не интерактивный текст. Если вы хотите его, проверьте код ниже ( не рекомендуется для большого текста ).
val uri = LocalUriHandler.current
FlowRow {
for (s in txt.split(' ')) {
if (s.matches(".*(#\\w+)|(http(s)?://.+).*".toRegex())) {
ClickableText(
text = AnnotatedString(s + ' '),
onClick = { runCatching { uri.openUri(s) } },
style = TextStyle(color = Color.Cyan)
)
} else {
Text(text = s + ' ')
}
}
}
И да, вам понадобится аккомпаниатор Flow_Layout .
Если вы хотите открывать только гиперссылки, существует динамический подход с использованиемHyperlinkText
Вы можете использовать этот код:
@Composable
fun AgreeConditionComponent(
modifier: Modifier,
value: Boolean,
onConditionChecked: (Boolean) -> Unit,
onConditionTextClicked: () -> Unit,
) {
val hyperlinkText = "link"
val annotatedString = buildAnnotatedString {
append("Click this")
append(" ")
val start = length
val end = length + hyperlinkText.length
addStringAnnotation(tag = "terms", annotation = "", start = start, end = end)
withStyle(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = Color(0xff64B5F6),
),
) {
append(hyperlinkText)
}
append(" ")
append("to go to web site")
}
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = value,
onCheckedChange = onConditionChecked,
)
Spacer(modifier = Modifier.width(4.dp))
ClickableText(
text = annotatedString,
style = MaterialTheme.typography.subtitle1,
onClick = { offset ->
annotatedString.getStringAnnotations(tag = "terms", start = offset, end = offset)
.firstOrNull()
?.let { onConditionTextClicked.invoke() }
}
)
}
}
Если вы хотите сделать часть текста кликабельной, то следующий код будет работать нормально.
@Composable
fun SingUpText() {
val annotatedText = buildAnnotatedString {
val grayStyle = SpanStyle(color = Color.Gray)
pushStyle(grayStyle)
append("Don't have an account? ")
pop()
pushStringAnnotation(
tag = "SignUp",
annotation = "SignUp"
)
val style = SpanStyle(color = AppColor, fontWeight = FontWeight.Bold)
pushStyle(style)
append("Sign Up")
pop()
}
ClickableText(text = annotatedText, onClick = {
annotatedText.getStringAnnotations(
tag = "SingUp",
start = it,
end = it
).firstOrNull().let { annotatedText ->
Log.d("Text_Clicked", "SingUpText:text ")
}
})
}