我想永久保存一个 Spanned 对象。(我现在一直保存它基于的字符串,但是在它上面运行 Html.fromHtml() 需要超过 1 秒,显着减慢了 UI。)
我看到 ParcelableSpan 和 SpannedString 和 SpannableString 之类的东西,但我不确定该使用哪个。
我想永久保存一个 Spanned 对象。(我现在一直保存它基于的字符串,但是在它上面运行 Html.fromHtml() 需要超过 1 秒,显着减慢了 UI。)
我看到 ParcelableSpan 和 SpannedString 和 SpannableString 之类的东西,但我不确定该使用哪个。
现在,Html.toHtml()
是您唯一的内置选项。Parcelable
用于进程间通信,并非设计为持久的。如果toHtml()
不涵盖您正在使用的所有特定类型的跨度,您将不得不制定自己的序列化机制。
由于保存对象涉及磁盘 I/O,因此无论如何都应该在后台线程中执行此操作,而不管toHtml()
.
我有类似的问题;我使用 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 标志,但是可以轻松调整此代码以适应其他类型。
我的用例是将 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 中,但目前这个版本似乎还不错。
从丹的想法:
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;
}
我想出的一个解决方案是使用带有自定义序列化器/解串器的 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.
我的用例是将 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 会给出正确的(正或负)整数。
这个问题很有趣,因为你必须从 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)