Spanned オブジェクトを永続的に保存したいと考えています。(現在、永続的に基づいている文字列を保存していますが、Html.fromHtml() を実行するのに 1 秒以上かかり、UI が著しく遅くなります。)
ParcelableSpan と SpannedString と SpannableString のようなものが表示されますが、どれを使用すればよいかわかりません。
Spanned オブジェクトを永続的に保存したいと考えています。(現在、永続的に基づいている文字列を保存していますが、Html.fromHtml() を実行するのに 1 秒以上かかり、UI が著しく遅くなります。)
ParcelableSpan と SpannedString と SpannableString のようなものが表示されますが、どれを使用すればよいかわかりません。
現時点でHtml.toHtml()は、 が唯一の組み込みオプションです。Parcelableプロセス間通信に使用され、永続的に設計されていません。使用しているすべての特定のタイプのスパンをカバーしていない場合toHtml()は、独自のシリアライゼーション メカニズムを作成する必要があります。
オブジェクトの保存にはディスク I/O が伴うため、toHtml().
私のユースケースは Spanned を Bundle に入れることでしたが、Google が私をここに連れてきました。@CommonsWare は、Parcelable が永続的なストレージには適していないことは正しいですが、バンドルに格納するには問題ありません。ほとんどのスパンは 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 にラップすることはおそらく可能ですが、このバージョンは今のところ問題ないようです。
同様の問題がありました。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 であることはわかっていましたが、このコードは他の型に対応するように簡単に変更できます。
私が思いついた解決策は、カスタムのシリアライザー/デシリアライザーで 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.