Объединение Spannable с String.format()

Предположим, у вас есть следующая строка:

String s = "The cold hand reaches for the %1$s %2$s Ellesse's";
String old = "old"; 
String tan = "tan"; 
String formatted = String.format(s,old,tan); //"The cold hand reaches for the old tan Ellesse's"

Предположим, вы хотите получить эту строку, но также иметь определенный Span установить для любого слова, замененного на String.format.

Например, мы также хотим сделать следующее:

Spannable spannable = new SpannableString(formatted);
spannable.setSpan(new StrikethroughSpan(), oldStart, oldStart+old.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ForegroundColorSpan(Color.BLUE), tanStart, tanStart+tan.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

Есть ли надежный способ узнать начальные индексы old и tan?

Обратите внимание, что просто поиск «старого» возвращает «старый» в «холодном», так что это не сработает.

Что будет работать, я думаю, это поиск %[0-9]$s заранее и вычисление смещений для учета замен в String.format. Это похоже на головную боль, я подозреваю, что может быть такой метод, как String.format, который более информативен в отношении особенностей его форматирования. Ну есть?


person Maarten    schedule 05.01.2014    source источник
comment
По крайней мере, для цвета вы можете попробовать строку замены <font color='blue'>tan</font> и использовать spannable=Html.fromHtml(formatted). Однако <strike>old</strike> не отображается.   -  person Maarten    schedule 05.01.2014
comment
Это почти наверняка выполнимо с помощью TextUtils.expandTemplate.   -  person Eric Cochran    schedule 23.10.2016


Ответы (5)


Я создал версию String.format это работает с spannables. Загрузите его и используйте как обычную версию. В вашем случае вы должны поместить промежутки вокруг спецификаторов формата (возможно, используя strings.xml). В выводе они будут примерно такими, какими были заменены эти спецификаторы.

person George Steel    schedule 30.11.2014
comment
Я попробовал это с индексом 1 (%1$s) только для одного аргумента, как в примере Android Developer, но мое приложение разбилось, поэтому я изменил его на 0, и это сработало. Итак, есть небольшое несоответствие. developer.android.com/guide/topics/resources/ - person SnapDragon; 09.12.2014
comment
@SnapDragon Я только что отправил новую фиксацию с исправлением. Спасибо, что нашли ошибку. - person George Steel; 20.12.2014

Использование Spannables, как это, является головной болью - это, вероятно, самый простой способ:

String s = "The cold hand reaches for the %1$s %2$s Ellesse's";
String old = "<font color=\"blue\">old</font>"; 
String tan = "<strike>tan</strike>"; 
String formatted = String.format(s,old,tan); //The cold hand reaches for the <font color="blue">old</font> <strike>tan</strike> Ellesse's

Spannable spannable = Html.fromHtml(formatted);

Проблема: это не ставит в StrikethroughSpan. Чтобы сделать StrikethroughSpan, мы заимствуем пользовательский TagHandler из этого вопроса.

Spannable spannable = Html.fromHtml(text,null,new MyHtmlTagHandler());

MyTagHandler:

public class MyHtmlTagHandler implements Html.TagHandler {
    public void handleTag(boolean opening, String tag, Editable output,
                          XMLReader xmlReader) {
        if (tag.equalsIgnoreCase("strike") || tag.equals("s")) {
            processStrike(opening, output);
        }
    }
    private void processStrike(boolean opening, Editable output) {
        int len = output.length();
        if (opening) {
            output.setSpan(new StrikethroughSpan(), len, len, Spannable.SPAN_MARK_MARK);
        } else {
            Object obj = getLast(output, StrikethroughSpan.class);
            int where = output.getSpanStart(obj);
            output.removeSpan(obj);
            if (where != len) {
                output.setSpan(new StrikethroughSpan(), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }

    private Object getLast(Editable text, Class kind) {
        Object[] objs = text.getSpans(0, text.length(), kind);
        if (objs.length == 0) {
            return null;
        } else {
            for (int i = objs.length; i > 0; i--) {
                if (text.getSpanFlags(objs[i - 1]) == Spannable.SPAN_MARK_MARK) {
                    return objs[i - 1];
                }
            }
            return null;
        }
    }
}
person Maarten    schedule 05.01.2014
comment
Это работает, но я думаю, что это неэффективно по сравнению. - person android developer; 02.11.2020

Я сделал простую функцию расширения Kotlin, которая должна решить String.format составную проблему:

fun Context.getStringSpanned(@StringRes resId: Int, vararg formatArgs: Any): Spanned {
    var lastArgIndex = 0
    val spannableStringBuilder = SpannableStringBuilder(getString(resId, *formatArgs))
    for (arg in formatArgs) {
        val argString = arg.toString()
        lastArgIndex = spannableStringBuilder.indexOf(argString, lastArgIndex)
        if (lastArgIndex != -1) {
            (arg as? CharSequence)?.let {
                spannableStringBuilder.replace(lastArgIndex, lastArgIndex + argString.length, it)
            }
            lastArgIndex += argString.length
        }
    }

    return spannableStringBuilder
}
person Pawel Cala    schedule 14.06.2019
comment
не уверен, что это решение правильное, потому что если у вас есть несколько spannables в ваших аргументах, например, с разными цветами, но с одним и тем же текстом, то они могут быть не в правильном порядке, потому что вы ищете текстовое содержимое для замены на spannable. кроме того, может появиться еще один пограничный случай, если в строке у вас есть подстрока с тем же содержимым, что и один из текста spannable, тогда ваша функция заменит неправильную позицию. см. одно из моих решений ниже, чтобы найти лучшее направление, по моему мнению. - person Raphael C; 28.04.2020

Мне нужно было заменить %s заполнителей в String набором Spannables, и я не нашел ничего достаточно удовлетворительного, поэтому я реализовал свой собственный форматер как расширение kotlin String. Надеюсь, это поможет кому-нибудь.

fun String.formatSpannable(vararg spans: CharSequence?): Spannable {
    val result = SpannableStringBuilder()
    when {
        spans.size != this.split("%s").size - 1 ->
            Log.e("formatSpannable",
                    "cannot format '$this' with ${spans.size} arguments")
        !this.contains("%s") -> result.append(this)
        else -> {
            var str = this
            var spanIndex = 0
            while (str.contains("%s")) {
                val preStr = str.substring(0, str.indexOf("%s"))
                result.append(preStr)
                result.append(spans[spanIndex] ?: "")
                str = str.substring(str.indexOf("%s") + 2)
                spanIndex++
            }
            if (str.isNotEmpty()) {
                result.append(str)
            }
        }
    }
    return result
}

а затем использовать следующим образом

"hello %s kotlin %s world".formatSpannable(span0, span1)
person Raphael C    schedule 28.04.2020

У меня возникла такая же проблема, но я нашел действительно отличное решение здесь: /36593916">Android: как объединить Spannable.setSpan с String.format?

Ознакомьтесь с решением, предложенным @george-steel. Он создал пользовательский версия String.format, которая сохраняет интервалы.

Пример использования:

Spanned toDisplay = SpanFormatter.format(getText(R.string.foo), bar, baz, quux);
person Chuck Taylor    schedule 17.03.2020