踩坑记录|EditText焦点问题造成IndexOutOfBoundsException setSpan (-1 ... -1) starts before 0

2,159 阅读2分钟

日志

java.lang.IndexOutOfBoundsException: setSpan (-1 ... -1) starts before 0
    at android.text.SpannableStringBuilder.checkRange(SpannableStringBuilder.java:1330)
    at android.text.SpannableStringBuilder.setSpan(SpannableStringBuilder.java:684)
    at android.text.SpannableStringBuilder.setSpan(SpannableStringBuilder.java:676)
    at android.text.Selection.setSelection(Selection.java:94)
    at android.text.Selection.setSelection(Selection.java:78)
    at android.text.Selection.setSelection(Selection.java:153)
    at android.widget.TextView.onDragEvent(TextView.java:12932)
    at android.view.View.callDragEventHandler(View.java:26953)
    at android.view.View.dispatchDragEvent(View.java:26941)
    at android.view.ViewGroup.dispatchDragEvent(ViewGroup.java:1839)
    at android.view.ViewGroup.dispatchDragEvent(ViewGroup.java:1839)
    at android.view.ViewGroup.dispatchDragEvent(ViewGroup.java:1839)
    at android.view.ViewGroup.dispatchDragEvent(ViewGroup.java:1839)
    at android.view.ViewGroup.dispatchDragEvent(ViewGroup.java:1839)
    at android.view.ViewGroup.dispatchDragEvent(ViewGroup.java:1839)
    at android.view.ViewGroup.dispatchDragEvent(ViewGroup.java:1839)
    at android.view.ViewGroup.dispatchDragEvent(ViewGroup.java:1839)
    at android.view.ViewGroup.dispatchDragEvent(ViewGroup.java:1839)
    at android.view.ViewGroup.dispatchDragEvent(ViewGroup.java:1839)
    at android.view.ViewRootImpl.handleDragEvent(ViewRootImpl.java:7430)
    at android.view.ViewRootImpl.access$1200(ViewRootImpl.java:203)
    at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:5248)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loop(Looper.java:236)
    at android.app.ActivityThread.main(ActivityThread.java:7879)
    at java.lang.reflect.Method.invoke(Method.java)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

前提条件

界面中有两个EditText,且都有内容,两个EditText都设置了OnFocusChangeListener,且在获取到焦点的时候,去改变EditText的typeface(特殊业务逻辑)。

复现路径

长按其中一个EditText,选中一段文字,再长按选中的文字,拖动文字到另外一个EditText,此时就会崩溃。

崩溃原因

看一下崩溃最后的地方的代码

// android.text.SpannableStringBuilder.checkRange(SpannableStringBuilder.java:1330)
private void checkRange(final String operation, int start, int end) {
    if (end < start) {
        throw new IndexOutOfBoundsException(operation + " " +
                region(start, end) + " has end before start");
    }

    int len = length();

    if (start > len || end > len) {
        throw new IndexOutOfBoundsException(operation + " " +
                region(start, end) + " ends beyond length " + len);
    }

    if (start < 0 || end < 0) {
        throw new IndexOutOfBoundsException(operation + " " +
                region(start, end) + " starts before 0");
    }
}

应该是传入的start或者end小于0了,那么往上翻一下start和end是哪来的

// android.text.Selection.setSelection(Selection.java:153)
public static final void setSelection(Spannable text, int index) {
    setSelection(text, index, index);
}

这里的start和end都传入的index,再往上看看index哪来的

// android.widget.TextView.onDragEvent(TextView.java:12932)
public boolean onDragEvent(DragEvent event) {
    switch (event.getAction()) {
        case DragEvent.ACTION_DRAG_STARTED:
            return mEditor != null && mEditor.hasInsertionController();

        case DragEvent.ACTION_DRAG_ENTERED:
            TextView.this.requestFocus();
            return true;

        case DragEvent.ACTION_DRAG_LOCATION:
            if (mText instanceof Spannable) {
                // 这里的offset就是传入的index
                final int offset = getOffsetForPosition(event.getX(), event.getY());
                Selection.setSelection(mSpannable, offset);
            }
            return true;

        case DragEvent.ACTION_DROP:
            if (mEditor != null) mEditor.onDrop(event);
            return true;

        case DragEvent.ACTION_DRAG_ENDED:
        case DragEvent.ACTION_DRAG_EXITED:
        default:
            return true;
    }
}

再看看getOffsetForPosition方法

// android.widget.TextView.getOffsetForPosition
public int getOffsetForPosition(float x, float y) {
    if (getLayout() == null) return -1;
    final int line = getLineAtCoordinate(y);
    final int offset = getOffsetAtCoordinate(line, x);
    return offset;
}

这里判断了一下layout是否为空,为空则返回-1,那应该就是layout为空了,看下设置typeface的方法

// android.widget.TextView.setTypeface
public void setTypeface(@Nullable Typeface tf) {
    if (mTextPaint.getTypeface() != tf) {
        mTextPaint.setTypeface(tf);

        if (mLayout != null) {
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }
}

这里貌似设置typeface后把layout置空了,看下nullLayouts方法

// android.widget.TextView.nullLayouts
public void nullLayouts() {
    if (mLayout instanceof BoringLayout && mSavedLayout == null) {
        mSavedLayout = (BoringLayout) mLayout;
    }
    if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
        mSavedHintLayout = (BoringLayout) mHintLayout;
    }

    // 将layout置空了
    mSavedMarqueeModeLayout = mLayout = mHintLayout = null;

    mBoring = mHintBoring = null;

    // Since it depends on the value of mLayout
    if (mEditor != null) mEditor.prepareCursorControllers();
}

最后真相大白了,其实就是在EditText获取到焦点的时候设置了typeface,然后TextView此时将Layout置空了,导致最后setSelection拿到的index==-1,抛出了异常。

解决方法

只需要去掉EditText获取到焦点时设置字体的逻辑就行了。