Jetpack Compose Text verlinkt einen Teil des Textes

Lesezeit: 11 Minuten

Benutzeravatar von Thracian
Thrakisch

Wie kann ich einem Abschnitt des Textes der Textkomponente einen Hyperlink hinzufügen?

Mit buildAnnotatedString ich kann einstellen Verknüpfung Abschnitt blau und unterstrichen wie im Bild unten, aber wie kann ich diesen Abschnitt auch in einen Link umwandeln?

Geben Sie hier die Bildbeschreibung ein

   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
    )

kann ich auch bekommen Spanned aber gibt es eine Möglichkeit, es mit zu verwenden Text?

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

Benutzeravatar von Thracian
Thrakisch

Für eine vollständige Antwort können Sie verwenden ClickableText die die Position des Textes zurückgibt, und UriHandler um URI in einem Browser zu öffnen.

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

  • Wie kann ich dies mit String-Ressourcen zum Laufen bringen, scheint ein großartiger Ansatz für hartcodierte Strings zu sein.

    – Guanaco-Entwickler

    7. Oktober 2021 um 20:24 Uhr

  • @GuanacoDevs schau dir meine Antwort unten an!

    – Chantell Osejo

    24. Mai um 21:50 Uhr

  • @ChantellOsejo Das scheint ein Weg zu sein, Sie können mehr Kontrolle erlangen. Diese Antwort führte mich jedoch zu einem vereinfachten Weg.

    – Guanaco-Entwickler

    26. Mai um 23:20 Uhr

Benutzeravatar von gaohomway
Gaohomway

Die markierte Antwort verwirrt Anfänger, ich gebe ein vollständiges Beispiel

Bitte das Ende nicht vergessen pushStringAnnotation mit 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)
    }
})

endgültige Wirkung

Geben Sie hier die Bildbeschreibung ein

  • Nett! Sie haben nur vergessen, pop() nach dem ersten pushStringAnnotation- und withStyle-Methodenpaar aufzurufen.

    – Zdenek Sima

    17. Dezember 2021 um 9:56 Uhr

  • Gibt es eine Möglichkeit, einen ausgewählten Zustand / Ripple-Effekt für eine solche Ansicht hinzuzufügen? Im Moment sieht es völlig statisch aus

    – Kostja Rudenko

    27. Januar um 12:24 Uhr

  • @ZdenekSima Danke für die Erinnerung, ich habe die Antwort aktualisiert

    – Gaohomway

    26. Februar um 4:31 Uhr

  • was ist ein pop()? warum brauchen wir das hier?

    – Michael Abyzov

    9. September um 11:36 Uhr

Für alle, die nach einer wiederverwendbaren Copy-Paste-Lösung suchen,

Erstellen Sie eine neue Datei LinkText.kt und diesen Code kopieren und einfügen,

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

Verwendungszweck

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

Bildschirmfoto,

Bildschirmfoto

Notiz

  1. Ich handhabe Webseiten manuell mit einem Composable. Verwenden UriHandler oder andere Alternativen, wenn eine manuelle Steuerung nicht erforderlich ist.
  2. Gestalten Sie anklickbaren und anderen Text nach Bedarf in LinkText.

  • Was ist die Verwendung von tag?

    – Marat

    16. Februar um 15:32 Uhr

  • @Marat, tag ist wie ich. Es wird verwendet, um Anmerkungen zu identifizieren – Dokumente: developer.android.com/reference/kotlin/androidx/compose/ui/text/…

    – Abhimanyu

    16. Februar um 16:07 Uhr

  • Aha. Vielen Dank

    – Marat

    16. Februar um 16:09 Uhr

  • @Abhimanyu, wenn Sie auf den unterstrichenen Text klicken, funktioniert es nicht

    – SpaceDevs

    23. Juni um 16:07 Uhr

Sie können verwenden https://github.com/firefinchdev/linkify-text

Es handelt sich um eine einzelne Datei, die Sie direkt in Ihr Projekt kopieren können.

Außerdem verwendet es Android Verlinken für die Verbindungserkennung, die mit der von identisch ist TextView‘s autoLink.

Benutzeravatar von Arda Kazancı
Arda Kazanci

Wie kann ich einem Abschnitt des Textes der Textkomponente einen Hyperlink hinzufügen?

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

Schild: Das Tag, das zur Unterscheidung von Anmerkungen verwendet wird

Anmerkung: Die angehängte Zeichenfolgenanmerkung

Anfang: Der inklusive Start-Offset des Bereichs

Ende: Der exklusive End-Offset der

Quelle

  • Was sind andere Tags außer “URL”?

    – Thrakisch

    4. Januar 2021 um 19:25 Uhr

  • Es tut uns leid. Ich habe es falsch verstanden. habe ich auch gerade gelernt. Es braucht 4 Parameter. Vielen Dank für Ihre nette Frage.

    – Arda Kazancı

    4. Januar 2021 um 20:18 Uhr

  • Ich habe dies mit annotatedString() versucht und auf gesetzt Text, Internetberechtigung zum Manifestieren hinzugefügt, aber es funktioniert nicht, ich meine, es passiert nichts, wenn Sie den Text berühren. Würdest du das überprüfen?

    – Thrakisch

    5. Januar 2021 um 6:45 Uhr


  • Sie müssen einen URL-Handler verwenden.

    – Arda Kazancı

    5. Januar 2021 um 11:14 Uhr

BEARBEITEN: Vor Jetpack Compose 1.3.0 gibt es einen Fehler, der verhindert, dass Dienste für die Barrierefreiheit eingebettete Links wie diese richtig lesen. Auch nach 1.3.0 gibt es einen weiteren Fehler, bei dem die Funktion onClick() nicht vom Accessibility Service (Talkback) aufgerufen wird. Sehen dieses Google-Problem. Ich würde empfehlen, die unten skizzierte AndroidView + TextView-Option der alten Schule zu verwenden, wenn Ihre App zugänglich sein muss, zumindest bis das verknüpfte Problem behoben ist.

Die Antworten hier sind alle großartig, wenn Sie hartcodierte Zeichenfolgen verwenden, aber sie sind für Zeichenfolgenressourcen nicht sehr nützlich. Hier ist ein Code, der Ihnen eine ähnliche Funktionalität bietet, wie TextViews der alten Schule mit HTML funktionieren würden, das vollständig mit Jetpack Compose erstellt wurde (keine Interop-APIs). Gutschrift für 99 % dieser Antwort geht an die Kommentar zu diesem Themadie ich erweitert habe, um die zu verwenden Anmerkungs-Tag für Android String-Ressourcen um URLs zu unterstützen. [Note: BulletSpan is not currently supported in this solution as it is not needed for my use case and I didn’t take the time to address its absence in the solution I extended]

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(
                                    annotation = 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,
    )
}

Verwendungszweck:

@Composable
fun MyComposableView {
    LinkableTextView(
        id = R.string.my_link_string
    )
}

String-Ressource:

<string name="my_link_string">Click this
    <annotation url="https://www.stackoverflow.com">link</annotation>
    to go to web site
</string>

Es gibt auch die “dumme” Möglichkeit, einfach auf die Verwendung von android.widget.TextView zurückzugreifen, die das gewünschte Verhalten aufweist und mit Zugänglichkeitsdiensten ordnungsgemäß funktioniert:

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

Für die Compat-Option ist es wichtig, dass Sie die String-Ressource wie beschrieben abrufen, damit die Tags nicht entfernt werden. Sie müssen Ihre String-Ressource auch mit CDATA-Tags formatieren, z

<string name="text_with_link"><![CDATA[Visit 
        <a href="https://www.stackoverflow.com/">Stackoverflow</a>
        for the best answers.]]></string>

Wenn das CDATA-Tag nicht verwendet wird, wird die Zeichenfolge nicht als HTML gerendert.

  • Was sind andere Tags außer “URL”?

    – Thrakisch

    4. Januar 2021 um 19:25 Uhr

  • Es tut uns leid. Ich habe es falsch verstanden. habe ich auch gerade gelernt. Es braucht 4 Parameter. Vielen Dank für Ihre nette Frage.

    – Arda Kazancı

    4. Januar 2021 um 20:18 Uhr

  • Ich habe dies mit annotatedString() versucht und auf gesetzt Text, Internetberechtigung zum Manifestieren hinzugefügt, aber es funktioniert nicht, ich meine, es passiert nichts, wenn Sie den Text berühren. Würdest du das überprüfen?

    – Thrakisch

    5. Januar 2021 um 6:45 Uhr


  • Sie müssen einen URL-Handler verwenden.

    – Arda Kazancı

    5. Januar 2021 um 11:14 Uhr

Benutzeravatar von Yasser AKBBACH
Yasser AKBBACH

Wenn es Ihr Anliegen ist, nur Hyperlinks zu öffnen, gibt es einen dynamischen Ansatz mit a HyperlinkText

1432100cookie-checkJetpack Compose Text verlinkt einen Teil des Textes

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy