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,请参阅我的其他答案
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)
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)
截屏,
笔记
UriHandler如果不需要手动控制,请使用或其他替代方案。LinkText。小智 12
您可以使用https://github.com/firefinchdev/linkify-text。
这是一个单一文件。您可以直接将其复制到您的项目中。
它使用 Android 的Linkify进行链接检测,这与TextView的autoLink相同。
如何将超链接添加到文本组件文本的某些部分?
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 : 的独占结束偏移量
@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 个扩展函数,它们使链接创建更加简单。
如果你想从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)
编辑:存在一个错误,该错误会阻止辅助服务正确读取嵌入式链接(例如 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。
| 归档时间: |
|
| 查看次数: |
2503 次 |
| 最近记录: |