web-dev-qa-db-fra.com

Comment utiliser des chaînes formatées avec des espaces réservés dans Android?

Dans Android il est possible d'utiliser des espaces réservés dans les chaînes, tels que:

<string name="number">My number is %1$d</string>

puis dans Java (à l'intérieur d'une sous-classe de Activity):

String res = getString(R.string.number);
String formatted = String.format(res, 5);

ou encore plus simple:

String formatted = getString(R.string.number, 5);

Il est également possible d'utiliser certaines balises HTML dans les ressources de chaîne Android:

<string name="underline"><u>Underline</u> example</string>

Puisque le String lui-même ne peut contenir aucune information sur le formatage, il faut utiliser la méthode getText(int) au lieu de getString(int):

CharSequence formatted = getText(R.string.underline);

Le CharSequence retourné peut ensuite être passé à Android, tels que TextView, et la phrase marquée sera soulignée.

Cependant, je n'ai pas pu trouver comment combiner ces deux méthodes, en utilisant une chaîne formatée avec des espaces réservés:

<string name="underlined_number">My number is <u>%1$d</u></string>

Comment traiter la ressource ci-dessus dans le code Java pour l'afficher dans un TextView, en remplaçant %1$d Par un entier?

44
maral

Enfin, j'ai réussi à trouver une solution de travail et j'ai écrit ma propre méthode pour remplacer les espaces réservés, en préservant le formatage:

public static CharSequence getText(Context context, int id, Object... args) {
    for(int i = 0; i < args.length; ++i)
        args[i] = args[i] instanceof String? TextUtils.htmlEncode((String)args[i]) : args[i];
    return Html.fromHtml(String.format(Html.toHtml(new SpannedString(context.getText(id))), args));
}

Cette approche ne nécessite pas d'échapper manuellement les balises HTML, ni dans une chaîne en cours de formatage, ni dans des chaînes qui remplacent les espaces réservés.

28
maral
<resources>
  <string name="welcome_messages">Hello, %1$s! You have &lt;b>%2$d new messages&lt;/b>.</string>
</resources>


Resources res = getResources();
String text = String.format(res.getString(R.string.welcome_messages), username, mailCount);
CharSequence styledText = Html.fromHtml(text);

Plus d'infos ici: http://developer.Android.com/guide/topics/resources/string-resource.html

16
Wagner Michael

Pour le cas simple où vous souhaitez remplacer un espace réservé sans mise en forme numérique (c'est-à-dire des zéros non significatifs, des chiffres après une virgule), vous pouvez utiliser la bibliothèque Square Phrase .

L'utilisation est très simple: vous devez d'abord modifier les espaces réservés dans votre ressource de chaîne en ce format plus simple:

<string name="underlined_number">My number is <u> {number} </u></string>

alors vous pouvez faire le remplacement comme ceci:

CharSequence formatted = Phrase.from(getResources(), R.string.underlined_number)
   .put("number", 5)
   .format()

Le CharSequence formaté est également stylisé. Si vous devez formater vos nombres, vous pouvez toujours les pré-formater à l'aide de String.format("%03d", 5), puis utiliser la chaîne résultante dans la fonction .put().

8
Genma

Fonction d'extension Kotlin

  • fonctionne avec toutes les versions d'API
  • gère plusieurs arguments

Exemple d'utilisation

textView.text = context.getText(R.string.html_formatted, "Hello in bold")

Ressource de chaîne HTML enveloppée dans une section CDATA

<string name="html_formatted"><![CDATA[ bold text: <B>%1$s</B>]]></string>

Résultat

texte en gras: Bonjour en gras

Code

/**
 * Create a formatted CharSequence from a string resource containing arguments and HTML formatting
 *
 * The string resource must be wrapped in a CDATA section so that the HTML formatting is conserved.
 *
 * Example of an HTML formatted string resource:
 * <string name="html_formatted"><![CDATA[ bold text: <B>%1$s</B> ]]></string>
 */
fun Context.getText(@StringRes id: Int, vararg args: Any?): CharSequence {
  val text = String.format(getString(id), *args)
  return if (Android.os.Build.VERSION.SDK_INT >= 24)
    Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT)
  else
    Html.fromHtml(text)
}
7
Frank Harper

Semblable à la réponse acceptée, j'ai essayé d'écrire une méthode d'extension Kotlin pour cela.

Voici la réponse acceptée dans Kotlin

@Suppress("DEPRECATION")
fun Context.getText(id: Int, vararg args: Any): CharSequence {
    val escapedArgs = args.map {
        if (it is String) TextUtils.htmlEncode(it) else it
    }.toTypedArray()
    return Html.fromHtml(String.format(Html.toHtml(SpannedString(getText(id))), *escapedArgs))
}

Le problème avec la réponse acceptée, c'est qu'elle ne semble pas fonctionner lorsque les arguments de format eux-mêmes sont stylisés (c'est-à-dire Spanned, pas String). Par expérience, cela semble faire des choses étranges, peut-être avec le fait que nous n'échappons pas aux CharSequences non-String. Je vois que si j'appelle

context.getText(R.id.my_format_string, myHelloSpanned)

où R.id.my_format_string est:

<string name="my_format_string">===%1$s===</string>

et myHelloSpanned est un Spanned qui ressemble à <b> bonjour </b> (c'est-à-dire qu'il aurait du HTML <i>&lt;b&gt;hello&lt;/b&gt;</i>) alors j'obtiens === bonjour=== (ie HTML ===<b>hello</b>===).

C'est faux, je devrais avoir === <b> bonjour </b> ===.

J'ai essayé de résoudre ce problème en convertissant toutes les CharSequences en HTML avant d'appliquer String.format, Et voici mon code résultant.

@Suppress("DEPRECATION")
fun Context.getText(@StringRes resId: Int, vararg formatArgs: Any): CharSequence {
    // First, convert any styled Spanned back to HTML strings before applying String.format. This
    // converts the styling to HTML and also does HTML escaping.
    // For other CharSequences, just do HTML escaping.
    // (Leave any other args alone.)
    val htmlFormatArgs = formatArgs.map {
        if (it is Spanned) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                Html.toHtml(it, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
            } else {
                Html.toHtml(it)
            }
        } else if (it is CharSequence) {
            Html.escapeHtml(it)
        } else {
            it
        }
    }.toTypedArray()

    // Next, get the format string, and do the same to that.
    val formatString = getText(resId);
    val htmlFormatString = if (formatString is Spanned) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            Html.toHtml(formatString, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
        } else {
            Html.toHtml(formatString)
        }
    } else {
        Html.escapeHtml(formatString)
    }

    // Now apply the String.format
    val htmlResultString = String.format(htmlFormatString, *htmlFormatArgs)

    // Convert back to a CharSequence, recovering any of the HTML styling.
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        Html.fromHtml(htmlResultString, Html.FROM_HTML_MODE_LEGACY)
    } else {
        Html.fromHtml(htmlResultString)
    }
}

Cependant, cela n'a pas vraiment fonctionné parce que lorsque vous appelez Html.toHtml, Il place des balises <p> Autour de tout, même lorsque ce remplissage supplémentaire n'était pas dans l'entrée. Autrement dit, Html.fromHtml(Html.toHtml(myHelloSpanned)) n'est pas égal à myHelloSpanned - il a un rembourrage supplémentaire. Je ne savais pas comment résoudre ça bien.

3
Chrispher

Voici une extension Kotlin plus lisible qui n'utilise pas d'API obsolètes, fonctionne sur toutes les versions Android et ne nécessite pas de chaînes à encapsuler dans les sections CDATA:

fun Context.getText(id: Int, vararg args: Any): CharSequence {

    val escapedArgs = args.map {
        if (it is String) TextUtils.htmlEncode(it) else it
    }.toTypedArray()

    val resource = SpannedString(getText(id))
    val htmlResource = HtmlCompat.toHtml(resource, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
    val formattedHtml = String.format(htmlResource, *escapedArgs)
    return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
}

Vous pouvez ajouter un alias en tant qu'extension de Fragment - n'oubliez pas de répartir les arguments entre:

fun Fragment.getText(id: Int, vararg args: Any) = requireContext().getText(id, *args)
0
Luke