12

我想永久保存一个 Spanned 对象。(我现在一直保存它基于的字符串,但是在它上面运行 Html.fromHtml() 需要超过 1 秒,显着减慢了 UI。)

我看到 ParcelableSpan 和 SpannedString 和 SpannableString 之类的东西,但我不确定该使用哪个。

4

7 回答 7

6

现在,Html.toHtml()是您唯一的内置选项。Parcelable用于进程间通信,并非设计为持久的。如果toHtml()不涵盖您正在使用的所有特定类型的跨度,您将不得不制定自己的序列化机制。

由于保存对象涉及磁盘 I/O,因此无论如何都应该在后台线程中执行此操作,而不管toHtml().

于 2012-05-12T19:11:55.470 回答
4

我有类似的问题;我使用 SpannableStringBuilder 来保存一个字符串和一堆跨度,我希望能够保存和恢复这个对象。我编写了这段代码以使用 SharedPreferences 手动完成此操作:

    // Save Log
    SpannableStringBuilder logText = log.getText();
    editor.putString(SAVE_LOG, logText.toString());
    ForegroundColorSpan[] spans = logText
            .getSpans(0, logText.length(), ForegroundColorSpan.class);
    editor.putInt(SAVE_LOG_SPANS, spans.length);
    for (int i = 0; i < spans.length; i++){
        int col = spans[i].getForegroundColor();
        int start = logText.getSpanStart(spans[i]);
        int end = logText.getSpanEnd(spans[i]);
        editor.putInt(SAVE_LOG_SPAN_COLOUR + i, col);
        editor.putInt(SAVE_LOG_SPAN_START + i, start);
        editor.putInt(SAVE_LOG_SPAN_END + i, end);
    }

    // Load Log
    String logText = save.getString(SAVE_LOG, "");
    log.setText(logText);
    int numSpans = save.getInt(SAVE_LOG_SPANS, 0);
    for (int i = 0; i < numSpans; i++){
        int col = save.getInt(SAVE_LOG_SPAN_COLOUR + i, 0);
        int start = save.getInt(SAVE_LOG_SPAN_START + i, 0);
        int end = save.getInt(SAVE_LOG_SPAN_END + i, 0);
        log.getText().setSpan(new ForegroundColorSpan(col), start, end, 
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

在我的情况下,我知道所有跨度都是 ForegroundColorSpan 类型并带有 SPAN_EXCLUSIVE_EXCLUSIVE 标志,但是可以轻松调整此代码以适应其他类型。

于 2013-11-14T12:14:51.073 回答
4

我的用例是将 Spanned 放入 Bundle 中,而 Google 将我带到了这里。@CommonsWare 是正确的,Parcelable 不适合持久存储,但可以存储到 Bundle 中。大多数跨度似乎扩展了 ParcelableSpan,因此这在 onSaveInstanceState 中对我有用:

ParcelableSpan spanObjects[] = mStringBuilder.getSpans(0, mStringBuilder.length(), ParcelableSpan.class);
int spanStart[] = new int[spanObjects.length];
int spanEnd[] = new int[spanObjects.length];
int spanFlags[] = new int[spanObjects.length];
for(int i = 0; i < spanObjects.length; ++i)
{
    spanStart[i] = mStringBuilder.getSpanStart(spanObjects[i]);
    spanEnd[i] = mStringBuilder.getSpanEnd(spanObjects[i]);
    spanFlags[i] = mStringBuilder.getSpanFlags(spanObjects[i]);
}

outState.putString("mStringBuilder:string", mStringBuilder.toString());
outState.putParcelableArray("mStringBuilder:spanObjects", spanObjects);
outState.putIntArray("mStringBuilder:spanStart", spanStart);
outState.putIntArray("mStringBuilder:spanEnd", spanEnd);
outState.putIntArray("mStringBuilder:spanFlags", spanFlags);

然后可以通过以下方式恢复状态:

mStringBuilder = new SpannableStringBuilder(savedInstanceState.getString("mStringBuilder:string"));
ParcelableSpan spanObjects[] = (ParcelableSpan[])savedInstanceState.getParcelableArray("mStringBuilder:spanObjects");
int spanStart[] = savedInstanceState.getIntArray("mStringBuilder:spanStart");
int spanEnd[] = savedInstanceState.getIntArray("mStringBuilder:spanEnd");
int spanFlags[] = savedInstanceState.getIntArray("mStringBuilder:spanFlags");
for(int i = 0; i < spanObjects.length; ++i)
    mStringBuilder.setSpan(spanObjects[i], spanStart[i], spanEnd[i], spanFlags[i]);

我在这里使用了 SpannableStringBuilder,但据我所知,它应该与任何实现 Spanned 的类一起使用。可能可以将此代码包装到 ParcelableSpanned 中,但目前这个版本似乎还不错。

于 2014-12-17T09:19:27.797 回答
2

从丹的想法:

public static String spannableString2JsonString(SpannableString ss) throws JSONException {
    JSONObject json = new JSONObject();
    json.put("text",ss.toString());
    JSONArray ja = new JSONArray();

    ForegroundColorSpan[] spans = ss.getSpans(0, ss.length(), ForegroundColorSpan.class);
    for (int i = 0; i < spans.length; i++){
        int col = spans[i].getForegroundColor();
        int start = ss.getSpanStart(spans[i]);
        int end = ss.getSpanEnd(spans[i]);
        JSONObject ij = new JSONObject();
        ij.put("color",col);
        ij.put("start",start);
        ij.put("end",end);
        ja.put(ij);
    }
    json.put("spans",ja);
    return json.toString();
}
public static SpannableString jsonString2SpannableString(String strjson) throws JSONException{
    JSONObject json = new JSONObject(strjson);
    SpannableString ss = new SpannableString(json.getString("text"));
    JSONArray ja = json.getJSONArray("spans");
    for (int i=0;i<ja.length();i++){
        JSONObject jo = ja.getJSONObject(i);
        int col = jo.getInt("color");
        int start = jo.getInt("start");
        int end = jo.getInt("end");
        ss.setSpan(new ForegroundColorSpan(col),start,end,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
    return ss;
}
于 2017-06-09T16:33:12.103 回答
0

我想出的一个解决方案是使用带有自定义序列化器/解串器的 GSON。该解决方案结合了其他答案中提到的一些想法。

定义一些 JSON 键

/* JSON Property Keys */
private static final String PREFIX = "SpannableStringBuilder:";
private static final String PROP_INPUT_STRING = PREFIX + "string";
private static final String PROP_SPAN_OBJECTS= PREFIX + "spanObjects";
private static final String PROP_SPAN_START= PREFIX + "spanStart";
private static final String PROP_SPAN_END = PREFIX + "spanEnd";
private static final String PROP_SPAN_FLAGS = PREFIX + "spanFlags";

Gson 串行器

public static class SpannableSerializer implements JsonSerializer<SpannableStringBuilder> {
    @Override
    public JsonElement serialize(SpannableStringBuilder spannableStringBuilder, Type type, JsonSerializationContext context) {
        ParcelableSpan[] spanObjects = spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), ParcelableSpan.class);

        int[] spanStart = new int[spanObjects.length];
        int[] spanEnd= new int[spanObjects.length];
        int[] spanFlags = new int[spanObjects.length];
        for(int i = 0; i < spanObjects.length; ++i) {
            spanStart[i] = spannableStringBuilder.getSpanStart(spanObjects[i]);
            spanEnd[i] = spannableStringBuilder.getSpanEnd(spanObjects[i]);
            spanFlags[i] = spannableStringBuilder.getSpanFlags(spanObjects[i]);
        }
        JsonObject jsonSpannable = new JsonObject();
        jsonSpannable.addProperty(PROP_INPUT_STRING, spannableStringBuilder.toString());
        jsonSpannable.addProperty(PROP_SPAN_OBJECTS, gson.toJson(spanObjects));
        jsonSpannable.addProperty(PROP_SPAN_START, gson.toJson(spanStart));
        jsonSpannable.addProperty(PROP_SPAN_END, gson.toJson(spanEnd));
        jsonSpannable.addProperty(PROP_SPAN_FLAGS, gson.toJson(spanFlags));
        return jsonSpannable;
    }
}

Gson 解串器

public static class SpannableDeserializer implements JsonDeserializer<SpannableStringBuilder> {
    @Override
    public SpannableStringBuilder deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
        JsonObject jsonSpannable = jsonElement.getAsJsonObject();
        try {
            String spannableString = jsonSpannable.get(PROP_INPUT_STRING).getAsString();
            SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(spannableString);
            String spanObjectJson = jsonSpannable.get(PROP_SPAN_OBJECTS).getAsString();
            ParcelableSpan[] spanObjects = gson.fromJson(spanObjectJson, ParcelableSpan[].class);
            String spanStartJson = jsonSpannable.get(PROP_SPAN_START).getAsString();
            int[] spanStart = gson.fromJson(spanStartJson, int[].class);
            String spanEndJson = jsonSpannable.get(PROP_SPAN_END).getAsString();
            int[] spanEnd = gson.fromJson(spanEndJson, int[].class);
            String spanFlagsJson = jsonSpannable.get(PROP_SPAN_FLAGS).getAsString();
            int[] spanFlags = gson.fromJson(spanFlagsJson, int[].class);
            for (int i = 0; i <spanObjects.length; ++i) {
                spannableStringBuilder.setSpan(spanObjects[i], spanStart[i], spanEnd[i], spanFlags[i]);
            }
            return spannableStringBuilder;
        } catch (Exception ex) {
            Log.e(TAG, Log.getStackTraceString(ex));
        }
        return null;
    }
}

因为ParcelableSpan您可能需要像这样将类型注册到 GSON:

RuntimeTypeAdapterFactory
  .of(ParcelableSpan.class)
  .registerSubtype(ForegroundColorSpan.class);
  .registerSubtype(StyleSpan.class); //etc.
于 2018-02-02T01:05:36.060 回答
0

我的用例是将 TextView 的内容(包括颜色和样式)转换为十六进制字符串/从十六进制字符串转换。根据 Dan 的回答,我想出了以下代码。希望如果有人有类似的用例,它会为您省去一些麻烦。

将 textBox 的内容存储到字符串:

String actualText = textBox.getText().toString();
SpannableString spanStr = new SpannableString(textBox.getText());

ForegroundColorSpan[] fSpans = spanStr.getSpans(0,spanStr.length(),ForegroundColorSpan.class);
StyleSpan[] sSpans = spanStr.getSpans(0,spanStr.length(),StyleSpan.class);

int nSpans = fSpans.length;
String spanInfo = "";
String headerInfo = String.format("%08X",nSpans);
for (int i = 0; i < nSpans; i++) {
    spanInfo += String.format("%08X",fSpans[i].getForegroundColor());
    spanInfo += String.format("%08X",spanStr.getSpanStart(fSpans[i]));
    spanInfo += String.format("%08X",spanStr.getSpanEnd(fSpans[i]));
}

nSpans = sSpans.length;
headerInfo += String.format("%08X",nSpans);
for (int i = 0; i < nSpans; i++) {
    spanInfo += String.format("%08X",sSpans[i].getStyle());
    spanInfo += String.format("%08X",spanStr.getSpanStart(sSpans[i]));
    spanInfo += String.format("%08X",spanStr.getSpanEnd(sSpans[i]));
}

headerInfo += spanInfo;
headerInfo += actualText;
return headerInfo;

从字符串中检索 textBox 的内容:

        String header = tvString.substring(0,8);
        int fSpans = Integer.parseInt(header,16);
        header = tvString.substring(8,16);
        int sSpans = Integer.parseInt(header,16);
        int nSpans = fSpans + sSpans;
        SpannableString tvText = new SpannableString(tvString.substring(nSpans*24+16));
        tvString = tvString.substring(16,nSpans*24+16);

        int cc, ss, ee;
        int begin;
        for (int i = 0; i < fSpans; i++) {
            begin = i*24;
            cc = (int) Long.parseLong(tvString.substring(begin,begin+8),16);
            ss = (int) Long.parseLong(tvString.substring(begin+8,begin+16),16);
            ee = (int) Long.parseLong(tvString.substring(begin+16,begin+24),16);
            tvText.setSpan(new ForegroundColorSpan(cc), ss, ee, 0);
        }
        for (int i = 0; i < sSpans; i++) {
            begin = i*24+fSpans*24;
            cc = (int) Long.parseLong(tvString.substring(begin,begin+8),16);
            ss = (int) Long.parseLong(tvString.substring(begin+8,begin+16),16);
            ee = (int) Long.parseLong(tvString.substring(begin+16,begin+24),16);
            tvText.setSpan(new StyleSpan(cc), ss, ee, 0);
        }

        textBox.setText(tvText);

检索代码中 (int) Long.parseLong 的原因是因为样式/颜色可以是负数。这会使 parseInt 跳闸并导致溢出错误。但是,执行 parseLong 然后转换为 int 会给出正确的(正或负)整数。

于 2020-09-19T03:26:30.563 回答
0

这个问题很有趣,因为你必须从 SpannableString 或 SpannableStringBuilder 中保存所有你想要的信息,Gson 不会自动选择它们。使用 HTML 对我的实现来说不能正常工作,所以这里有另一个可行的解决方案。这里的所有答案都不完整,您必须执行以下操作:

class SpannableSerializer : JsonSerializer<SpannableStringBuilder?>, JsonDeserializer<SpannableStringBuilder?> {

    private val gson: Gson
        get() {
            val rtaf = RuntimeTypeAdapterFactory
                    .of(ParcelableSpan::class.java, ParcelableSpan::class.java.simpleName)
                    .registerSubtype(ForegroundColorSpan::class.java, ForegroundColorSpan::class.java.simpleName)
                    .registerSubtype(StyleSpan::class.java, StyleSpan::class.java.simpleName)
                    .registerSubtype(RelativeSizeSpan::class.java, RelativeSizeSpan::class.java.simpleName)
                    .registerSubtype(SuperscriptSpan::class.java, SuperscriptSpan::class.java.simpleName)
                    .registerSubtype(UnderlineSpan::class.java, UnderlineSpan::class.java.simpleName)
            return GsonBuilder()
                    .registerTypeAdapterFactory(rtaf)
                    .create()
        }

    override fun serialize(spannableStringBuilder: SpannableStringBuilder?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
        val spanTypes = spannableStringBuilder?.getSpans(0, spannableStringBuilder.length, ParcelableSpan::class.java)
        val spanStart = IntArray(spanTypes?.size ?: 0)
        val spanEnd = IntArray(spanTypes?.size ?: 0)
        val spanFlags = IntArray(spanTypes?.size ?: 0)
        val spanInfo = DoubleArray(spanTypes?.size ?: 0)
        spanTypes?.forEachIndexed { i, span ->
            when (span) {
                is ForegroundColorSpan -> spanInfo[i] = span.foregroundColor.toDouble()
                is StyleSpan -> spanInfo[i] = span.style.toDouble()
                is RelativeSizeSpan -> spanInfo[i] = span.sizeChange.toDouble()
            }
            spanStart[i] = spannableStringBuilder.getSpanStart(span)
            spanEnd[i] = spannableStringBuilder.getSpanEnd(span)
            spanFlags[i] = spannableStringBuilder.getSpanFlags(span)
        }

        val jsonSpannable = JsonObject()
        jsonSpannable.addProperty(INPUT_STRING, spannableStringBuilder.toString())
        jsonSpannable.addProperty(SPAN_TYPES, gson.toJson(spanTypes))
        jsonSpannable.addProperty(SPAN_START, gson.toJson(spanStart))
        jsonSpannable.addProperty(SPAN_END, gson.toJson(spanEnd))
        jsonSpannable.addProperty(SPAN_FLAGS, gson.toJson(spanFlags))
        jsonSpannable.addProperty(SPAN_INFO, gson.toJson(spanInfo))
        return jsonSpannable
    }

    override fun deserialize(jsonElement: JsonElement, type: Type, jsonDeserializationContext: JsonDeserializationContext): SpannableStringBuilder {
        val jsonSpannable = jsonElement.asJsonObject
        val spannableString = jsonSpannable[INPUT_STRING].asString
        val spannableStringBuilder = SpannableStringBuilder(spannableString)
        val spanObjectJson = jsonSpannable[SPAN_TYPES].asString
        val spanTypes: Array<ParcelableSpan> = gson.fromJson(spanObjectJson, Array<ParcelableSpan>::class.java)
        val spanStartJson = jsonSpannable[SPAN_START].asString
        val spanStart: IntArray = gson.fromJson(spanStartJson, IntArray::class.java)
        val spanEndJson = jsonSpannable[SPAN_END].asString
        val spanEnd: IntArray = gson.fromJson(spanEndJson, IntArray::class.java)
        val spanFlagsJson = jsonSpannable[SPAN_FLAGS].asString
        val spanFlags: IntArray = gson.fromJson(spanFlagsJson, IntArray::class.java)
        val spanInfoJson = jsonSpannable[SPAN_INFO].asString
        val spanInfo: DoubleArray = gson.fromJson(spanInfoJson, DoubleArray::class.java)
        for (i in spanTypes.indices) {
            when (spanTypes[i]) {
                is ForegroundColorSpan -> spannableStringBuilder.setSpan(ForegroundColorSpan(spanInfo[i].toInt()), spanStart[i], spanEnd[i], spanFlags[i])
                is StyleSpan -> spannableStringBuilder.setSpan(StyleSpan(spanInfo[i].toInt()), spanStart[i], spanEnd[i], spanFlags[i])
                is RelativeSizeSpan -> spannableStringBuilder.setSpan(RelativeSizeSpan(spanInfo[i].toFloat()), spanStart[i], spanEnd[i], spanFlags[i])
                else -> spannableStringBuilder.setSpan(spanTypes[i], spanStart[i], spanEnd[i], spanFlags[i])
            }
        }
        return spannableStringBuilder
    }

    companion object {
        private const val PREFIX = "SSB:"
        private const val INPUT_STRING = PREFIX + "string"
        private const val SPAN_TYPES = PREFIX + "spanTypes"
        private const val SPAN_START = PREFIX + "spanStart"
        private const val SPAN_END = PREFIX + "spanEnd"
        private const val SPAN_FLAGS = PREFIX + "spanFlags"
        private const val SPAN_INFO = PREFIX + "spanInfo"
    }
}

如果还有其他类型的跨度,您必须在 when 部分添加它们并选择跨度的相关信息,很容易将它们全部添加。

RuntimeTypeAdapterFactory 在 gson 库中是私有的,您必须将其复制到您的项目中。 https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java

现在使用它!

val gson by lazy {
    val type: Type = object : TypeToken<SpannableStringBuilder>() {}.type
    GsonBuilder()
            .registerTypeAdapter(type, SpannableSerializer())
            .create()
}
val ssb = gson.fromJson("your json here", SpannableStringBuilder::class.java)
于 2021-02-21T18:19:36.720 回答