Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android: How to combine Spannable.setSpan with String.format?

I'm setting Span to part of text. Span itself works well. However the text is created by String.format from Resources and I do not know start and end of part in the text i'm going to set Span to.

I tryed to use custom HTML tags in strings.xml, but either getText or getString remove them. I could use something like this getString(R.string.text, "<nb>" + arg + "</nb>"), then Html.fromHtml(), because the arg is exactly where i want to set the Span.

I seen this approach that used text formated "normal text ##span here## normal text". It parses the string removes tags and sets Span.

My question is, is there better way to accomplish setting Span into formated string like "something %s something" or should i use one of the above approaches?

like image 788
Salw Avatar asked Aug 26 '11 10:08

Salw


People also ask

How do you make Spannable strings?

Add a Bulleted list in Android. With the help of BulletSpan , you can create a bullet list in your application to display some information in a short and simple way. So, in this way, we can use Spans to style the texts present in our application.

What is a difference between Spannable and string?

A Spannable allows to attach formatting information like bold, italic, ... to sub-sequences ("spans", thus the name) of the characters. It can be used whenever you want to represent "rich text". The Html class provides an easy way to construct such text, for example: Html.

How do you do Spannable text on Android?

To apply a span, call setSpan(Object _what_, int _start_, int _end_, int _flags_) on a Spannable object. The what parameter refers to the span to apply to the text, while the start and end parameters indicate the portion of the text to which to apply the span.


3 Answers

getText() will return SpannedString objects that contain the formatting defined in strings.xml. I have created a custom version of String.format that will preserve any spans in the format string, even of they enclose format specifiers (spans in SpannedString arguments are also preserved). Use it like this:

Spanned toDisplay = SpanFormatter.format(getText(R.string.foo), bar, baz, quux);
like image 187
George Steel Avatar answered Sep 27 '22 16:09

George Steel


I solved this by introducing TaggedArg class, instances of this class expands to <tag>value</tag>. Then I created object that is responsible for reading text containing tags and replacing these tags by spans. Different spans are registered in map tag->factory.

There was one little surprise. If you have text like "<xx>something</xx> something", Html.fromHtml reads this text as "<xx>something something</xx>". I had to add tags <html> around whole text to prevent this.

like image 40
Salw Avatar answered Sep 27 '22 16:09

Salw


I've decided to write a Kotlin version of what was offered here by George, in case the link goes away some day:

/*
* Copyright © 2014 George T. Steel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//https://github.com/george-steel/android-utils/blob/master/src/org/oshkimaadziig/george/androidutils/SpanFormatter.java
/**
 * Provides [String.format] style functions that work with [Spanned] strings and preserve formatting.
 *
 * @author George T. Steel
 */
object SpanFormatter {
    private val FORMAT_SEQUENCE: Pattern = Pattern.compile("%([0-9]+\\$|<?)([^a-zA-z%]*)([[a-zA-Z%]&&[^tT]]|[tT][a-zA-Z])")

    /**
     * Version of [String.format] that works on [Spanned] strings to preserve rich text formatting.
     * Both the `format` as well as any `%s args` can be Spanned and will have their formatting preserved.
     * Due to the way [android.text.Spannable]s work, any argument's spans will can only be included **once** in the result.
     * Any duplicates will appear as text only.
     *
     * @param format the format string (see [java.util.Formatter.format])
     * @param args
     * the list of arguments passed to the formatter. If there are
     * more arguments than required by `format`,
     * additional arguments are ignored.
     * @return the formatted string (with spans).
     */
    fun format(format: CharSequence?, vararg args: Any?): SpannedString {
        return format(java.util.Locale.getDefault(), format, *args)
    }

    /**
     * Version of [String.format] that works on [Spanned] strings to preserve rich text formatting.
     * Both the `format` as well as any `%s args` can be Spanned and will have their formatting preserved.
     * Due to the way [android.text.Spannable]s work, any argument's spans will can only be included **once** in the result.
     * Any duplicates will appear as text only.
     *
     * @param locale
     * the locale to apply; `null` value means no localization.
     * @param format the format string (see [java.util.Formatter.format])
     * @param args
     * the list of arguments passed to the formatter.
     * @return the formatted string (with spans).
     * @see String.format
     */
    fun format(locale: java.util.Locale, format: CharSequence?, vararg args: Any?): SpannedString {
        val out = SpannableStringBuilder(format)
        var i = 0
        var argAt: Int = -1
        while (i < out.length) {
            val m: java.util.regex.Matcher = FORMAT_SEQUENCE.matcher(out)
            if (!m.find(i))
                break
            i = m.start()
            val exprEnd: Int = m.end()
            val argTerm: String? = m.group(1)
            val modTerm: String? = m.group(2)
            val typeTerm: String? = m.group(3)
            var cookedArg: CharSequence
            when (typeTerm) {
                "%" -> cookedArg = "%"
                "n" -> cookedArg = "\n"
                else -> {
                    val argIdx: Int = when (argTerm) {
                        "" -> ++argAt
                        "<" -> argAt
                        else -> argTerm!!.substring(0, argTerm.length - 1).toInt() - 1
                    }
                    val argItem: Any? = args[argIdx]
                    cookedArg = if ((typeTerm == "s") && argItem is Spanned) {
                        argItem
                    } else {
                        String.format(locale, "%$modTerm$typeTerm", argItem)
                    }
                }
            }
            out.replace(i, exprEnd, cookedArg)
            i += cookedArg.length
        }
        return SpannedString(out)
    }
}
like image 27
android developer Avatar answered Sep 27 '22 16:09

android developer