Jetpack Compose Text 超链接文本的某些部分

Thr*_*ian 10 android android-jetpack-compose android-jetpack-compose-text

如何将超链接添加到 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
    )
Run Code Online (Sandbox Code Playgroud)

我也可以,Spanned但有什么方法可以使用它Text吗?

val str: Spanned = HtmlCompat.fromHtml(
    "<a href=\"http://www.github.com\">Github</a>", HtmlCompat.FROM_HTML_MODE_LEGACY
)
Run Code Online (Sandbox Code Playgroud)

gao*_*way 94

标注的答案让新手很困惑,我给出一个完整的例子

请不要忘记pushStringAnnotation pop()

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")
    }
    pop()

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

Run Code Online (Sandbox Code Playgroud)

最终效果

在此输入图像描述

如果您需要#tags和@mentions,请参阅我的其他答案

在此输入图像描述

  • 好的!您只是忘记在第一个 pushStringAnnotation 和 withStyle 方法对之后调用 pop() 。 (4认同)
  • 有没有办法为这样的视图添加一些选定的状态/波纹效果?现在它看起来完全静态 (2认同)
  • 什么是“pop()”?为什么我们需要这个? (2认同)

Thr*_*ian 23

对于完整的答案,您可以使用ClickableTextwhich 返回文本的位置,并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 = LocalUriHandler.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)
            }
    }
)
Run Code Online (Sandbox Code Playgroud)

  • 我如何才能使用字符串资源来实现这一点,这对于硬编码字符串来说似乎是一个很好的方法。 (5认同)

Abh*_*bhi 23

对于任何正在寻找可重复使用的复制粘贴解决方案的人来说,

创建一个新文件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)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法

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,
        ),
)
Run Code Online (Sandbox Code Playgroud)

截屏

截屏

笔记

  1. 我正在使用可组合项手动处理网页。UriHandler如果不需要手动控制,请使用或其他替代方案。
  2. 根据需要设计可点击的文本和其他文本LinkText


小智 12

您可以使用https://github.com/firefinchdev/linkify-text

这是一个单一文件。您可以直接将其复制到您的项目中。

它使用 Android 的Linkify进行链接检测,这与TextViewautoLink相同。


sno*_*lax 8

如何将超链接添加到文本组件文本的某些部分?

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
    )
}
Run Code Online (Sandbox Code Playgroud)

tag : 用于区分注释的标签

注释:附加的字符串注释

start : 范围的起始偏移量

end : 的独占结束偏移量

来源


vov*_*ost 8

最简单、最干净的解决方案:

在此输入图像描述

@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)
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意 2 个扩展函数,它们使链接创建更加简单。


sla*_*boy 7

如果你想从strings.xml文件中使用@StringRes,你可以使用下面的代码

在此输入图像描述

假设您有以下字符串资源:

<?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>
Run Code Online (Sandbox Code Playgroud)

你可以这样使用它:

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
            }
        )
    )
)
Run Code Online (Sandbox Code Playgroud)

这是可组合项的源代码:

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
    )
}
Run Code Online (Sandbox Code Playgroud)


Cha*_*ejo 6

编辑:存在一个错误,该错误会阻止辅助服务正确读取嵌入式链接(例如 Jetpack Compose 1.3.0 之前的链接)。即使在 1.3.0 之后,还有另一个错误,即 Accessibility Service (Talkback) 不调用 onClick() 函数。请参阅此 Google 问题。如果您的应用程序需要可访问,我建议使用我在下面概述的 AndroidView + 老式 TextView 选项,至少在链接的问题得到解决之前是这样。

--

如果您使用硬编码字符串,这里的答案都很好,但它们对于字符串资源不是很有用。下面的一些代码为您提供了与老式 TextView 与完全使用 Jetpack Compose(无互操作 API)构建的 HTML 类似的功能。这个答案的 99% 归功于对此问题的评论,我将其扩展为使用Android String 资源注释标记来支持 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
                                )
                                addUrlAnnotation(
                                    urlAnnotation = UrlAnnotation(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,
    )
}
Run Code Online (Sandbox Code Playgroud)

用法:

@Composable
fun MyComposableView {
    LinkableTextView(
        id = R.string.my_link_string
    )
}
Run Code Online (Sandbox Code Playgroud)

字符串资源:

<string name="my_link_string">Click this
    <annotation url="https://www.stackoverflow.com">link</annotation>
    to go to web site
</string>
Run Code Online (Sandbox Code Playgroud)

还有一种“愚蠢”的方式,就是退回到使用 android.widget.TextView ,它具有您正在寻找的行为,并且可以正确地与辅助服务一起使用:

@Composable
fun CompatHtmlTextView(@StringRes htmlStringResource: Int) {
    val html = stringResourceWithStyling(htmlStringResource).toString()

    AndroidView(factory = { context ->
        android.widget.TextView(context).apply {
            text = fromHtml(html)
        }
    })
}

@Composable
@ReadOnlyComposable
fun stringResWithStyling(@StringRes id: Int): CharSequence =
    LocalContext.current.resources.getText(id = id) 

/**
 * Compat method that will use the deprecated fromHtml method 
 * prior to Android N and the new one after Android N
 */
@Suppress("DEPRECATION")
fun fromHtml(html: String?): Spanned {
    return when {
        html == null -> {
            // return an empty spannable if the html is null
            SpannableString("")
        } 
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
            // FROM_HTML_MODE_LEGACY is the behaviour that was used for versions below android N
            // we are using this flag to give a consistent behaviour
            Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY)
        }
        else -> {
            Html.fromHtml(html)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

对于 Compat 选项,重要的是按照概述检索字符串资源,以便标签不会被剥离。您还必须使用 CDATA 标签格式化字符串资源,例如

<string name="text_with_link"><![CDATA[Visit 
        <a href="https://www.stackoverflow.com/">Stackoverflow</a>
        for the best answers.]]></string>
Run Code Online (Sandbox Code Playgroud)

如果不使用 CDATA 标记,则不会将字符串呈现为 HTML。