将自定义 Editable 与 EditText 结合使用会使编辑时出现故障

Using custom Editable with EditText makes it glitchy while editing

注意: 我创建了一个 GitHub 存储库,其中包含此错误的重现 here. Feel free to clone and try the app out yourself to see the bug. The relevant code is here:注释部分保留在注释中它有效好吧,取消注释,你就会遇到这个错误。


我正在为 Android 构建源代码编辑器应用程序。我有一个包含 SpannableStringBuilder 的自定义 Editable 类型(此后将称为 SSB)。这是它的代码:

package com.bluejay.myapplication;

import android.text.Editable;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;

public class ColoredText implements Editable {
    private final SpannableStringBuilder builder;

    public ColoredText(String rawText) {
        assert rawText != null;
        this.builder = new SpannableStringBuilder(rawText);
    }

    @Override
    public Editable replace(int st, int en, CharSequence source, int start, int end) {
        this.builder.replace(st, en, source, start, end);
        return this;
    }

    @Override
    public Editable replace(int st, int en, CharSequence text) {
        this.builder.replace(st, en, text);
        return this;
    }

    @Override
    public Editable insert(int where, CharSequence text, int start, int end) {
        this.builder.insert(where, text, start, end);
        return this;
    }

    @Override
    public Editable insert(int where, CharSequence text) {
        this.builder.insert(where, text);
        return this;
    }

    @Override
    public Editable delete(int st, int en) {
        this.builder.delete(st, en);
        return this;
    }

    @Override
    public Editable append(CharSequence text) {
        this.builder.append(text);
        return this;
    }

    @Override
    public Editable append(CharSequence text, int start, int end) {
        this.builder.append(text, start, end);
        return this;
    }

    @Override
    public Editable append(char text) {
        this.builder.append(text);
        return this;
    }

    @Override
    public void clear() {
        this.builder.clear();
    }

    @Override
    public void clearSpans() {
        this.builder.clearSpans();
    }

    @Override
    public void setFilters(InputFilter[] filters) {
        this.builder.setFilters(filters);
    }

    @Override
    public InputFilter[] getFilters() {
        return this.builder.getFilters();
    }

    @Override
    public void getChars(int start, int end, char[] dest, int destoff) {
        this.builder.getChars(start, end, dest, destoff);
    }

    @Override
    public void setSpan(Object what, int start, int end, int flags) {
        this.builder.setSpan(what, start, end, flags);
    }

    @Override
    public void removeSpan(Object what) {
        this.builder.removeSpan(what);
    }

    @Override
    public <T> T[] getSpans(int start, int end, Class<T> type) {
        return this.builder.getSpans(start, end, type);
    }

    @Override
    public int getSpanStart(Object tag) {
        return this.builder.getSpanStart(tag);
    }

    @Override
    public int getSpanEnd(Object tag) {
        return this.builder.getSpanEnd(tag);
    }

    @Override
    public int getSpanFlags(Object tag) {
        return this.builder.getSpanFlags(tag);
    }

    @Override
    public int nextSpanTransition(int start, int limit, Class type) {
        return this.builder.nextSpanTransition(start, limit, type);
    }

    @Override
    public int length() {
        return this.builder.length();
    }

    @Override
    public char charAt(int index) {
        return this.builder.charAt(index);
    }

    @Override
    public CharSequence subSequence(int start, int end) {
        return this.builder.subSequence(start, end);
    }
}

如您所见,此类型是 SSB 的简单包装器。 new ColoredText(str)str 及其所有方法调用(appenddelete 等除外,其中 return this 而不是SSB) 只需转发给 SSB。

现在,当我有一个 EditText 时,我尝试将 ColoredText 设置为 EditText 的基础文本,就像这样

EditText editText = (EditText) findViewById(R.id.editText);
// By default, setText() will attempt to copy the passed CharSequence into a new SSB.
// See https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/TextView.java#L4396
// and https://github.com/android/platform_frameworks_base/blob/master/core/java/android/text/Editable.java#L143
// I want to prevent this and have the ColoredText instead of an SSB be the EditText's
// underlying text, that is, I want the mText member to be of type ColoredText.
editText.setEditableFactory(new Editable.Factory() {
    @Override
    public Editable newEditable(CharSequence source) {
        return (Editable) source; // source is ColoredText
    }
});

ColoredText text = new ColoredText("Hello world!\nHello world again!");
editText.setText(text, TextView.BufferType.EDITABLE);

EditText 在编辑时会出现很多问题。在上面的示例中,点击带有 Hello world! 的第一行的任意位置并开始输入随机字符。第二行将受到影响,并且不知何故(即使您没有触摸换行键或箭头键)光标最终会溢出到第二行。并且您键入的某些字符可能不会显示,即使光标会移动。

现在,如果您注释掉 setEditableFactory 部分,那么文本会在 setText() 期间被复制到 SSB 中,然后您再次 运行 应用程序,您将看到没有故障。

如果您保留 setEditableFactory 部分不变,但将 text 的变量初始化替换为

,它甚至可以工作
SpannableStringBuilder text = new SpannableStringBuilder("Hello world!\nHello world again!");

很明显,虽然 setText() 说它会接受任何 Editable,但它在处理 SSB 以外的任何东西时效果不佳。为什么会发生这种情况,我该如何解决?谢谢

通过挖掘SpannableStringBuilder的源代码,我发现它不仅完成了接口Editable等定义的职责,而且还通过调用[=12=报告了跨度变化] 通过 thisDynamicLayoutEditText 的真正主力)通过检查传入引用与其成员(这是我们实际的 ColoredSpan 实例)是否相等来响应 onSpanChanged()。显然它们是不同的,我怀疑这是一个问题。

其实SpannableStringBuilder不只是Editable,而是更多。如果您需要自定义 Editable 子类化 SpannableStringBuilder 可能有效。