RecyclerView复用EditText后长按不可选中

最近做了一个图文混排的编辑功能,想到了用RecyclerView设置不同的ViewType,实现EditText和ImageView的混排效果。如图:

RecyclerView复用EditText后长按不可选中_第1张图片

但有一个问题困扰了我很久,就是编辑少量内容的时候正常,当编辑的内容多了,EditText和ImageView都会被复用,复用会导致我长按EditText不会弹出复制、粘贴、全选等功能菜单了,于是苦思冥想去找出问题的原因,此篇文章是基于上一篇 EditText是如何实现长按弹出复制粘贴等ContextMenu的源码解析,如果没看过的话,希望能去看一下,不然看本篇文章会有一些不自然。

要想找到问题的原因就得debug,入口呢?就是上篇文章提到的selectCurrentWordAndStartDrag()这个方法

private boolean selectCurrentWordAndStartDrag() {
        if (mInsertionActionModeRunnable != null) {
            mTextView.removeCallbacks(mInsertionActionModeRunnable);
        }
        if (extractedTextModeWillBeStarted()) {
            return false;
        }
        if (!checkField()) {
            return false;
        }
        if (!mTextView.hasSelection() && !selectCurrentWord()) {
            // No selection and cannot select a word.
            return false;
        }
        stopTextActionModeWithPreservingSelection();
        getSelectionController().enterDrag(
                SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
        return true;
    }

我发现被复用的EditText在checkField()的时候返回的是false,从而导致了这个方法进行不下去了,这是问题的切入点。我们看看checkField()方法:

	/**
     * Checks whether a selection can be performed on the current TextView.
     *
     * @return true if a selection can be performed
     */
    boolean checkField() {
        if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
            Log.w(TextView.LOG_TAG,
                    "TextView does not support text selection. Selection cancelled.");
            return false;
        }
        return true;
    }

这个方法的作用是检测在当前的TextView中是否可以执行选中,mTextView.requestFocus()是没有问题的,问题出在mTextView.canSelectText(),于是进入到canSelectText()方法:

boolean canSelectText() {
        return mText.length() != 0 && mEditor != null && mEditor.hasSelectionController();
    }

这个方法很简单,debug 显示mEditor.hasSelectionController()返回为false,通过上一篇文章可以知道正常情况下mEditor的SelectionController是SelectionModifierCursorController,这里为啥返回为false呢?进去看看:

    boolean hasSelectionController() {
        return mSelectionControllerEnabled;
    }

只是返回了一个变量mSelectionControllerEnabled,想必是mSelectionControllerEnabled在被复用的时候被设置为了false,搜索一下这个变量在Editor中是在哪个地方赋值的,结果发现在这个方法里:


    void prepareCursorControllers() {
        boolean windowSupportsHandles = false;
		//获取mTextView的布局属性
        ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
        //如果布局属性为WindowManager.LayoutParams才能执行
        if (params instanceof WindowManager.LayoutParams) {
            WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
            windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
                    || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
        }

        boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
        mInsertionControllerEnabled = enabled && isCursorVisible();
        //关键的赋值语句
        mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();

        if (!mInsertionControllerEnabled) {
            hideInsertionPointCursorController();
            if (mInsertionPointCursorController != null) {
                mInsertionPointCursorController.onDetached();
                mInsertionPointCursorController = null;
            }
        }

        if (!mSelectionControllerEnabled) {
            stopTextActionMode();
            if (mSelectionModifierCursorController != null) {
                mSelectionModifierCursorController.onDetached();
                mSelectionModifierCursorController = null;
            }
        }
    }

mSelectionControllerEnabled 的值取决于enabled && mTextView.textCanBeSelected(); 从debug上看 mTextView.textCanBeSelected();返回的是true,那问题就出在enabled 喽,enabled = windowSupportsHandles && mTextView.getLayout() != null; debug显示windowSupportsHandles 值为false,windowSupportsHandles 默认为false,赋值的地方就在if语句中,难道赋值为false了,还是根本就没有执行赋值语句呢?反复进行了几次debug发现都没有进入if语句中。

问题的关键来了,正常情况下mTextView.getRootView()返回的是DecorView,DecorView的LayoutParams类型就是WindowManager.LayoutParams,所以能执行if语句,被复用后的mTextView.getRootView()返回的并不是DecorView,而是EditText自己,为什么会出现这种情况呢?getRootView()这个方法是位于View中的:

public View getRootView() {
        if (mAttachInfo != null) {
            //正常情况下mAttachInfo.mRootView就是DecorView
            final View v = mAttachInfo.mRootView;
            if (v != null) {
                return v;
            }
        }
		
        View parent = this;

        while (parent.mParent != null && parent.mParent instanceof View) {
            parent = (View) parent.mParent;
        }

        return parent;
    }

mAttachInfo是在AttachedToWindow的时候赋值的,结果发现mAttachInfo为空,所以才不会执行mAttachInfo.mRootView,而返回this;那么为什么mAttachInfo 为空呢,这里我没有去研究RecyclerView(懒),但可以肯定的是RecyclerView复用EditText时候没有做AttachedToWindow的操作从而导致mAttachInfo 为空。

那如何解决这个复用问题呢?不复用。卧槽,搞了半天你没有解决问题啊(不要打我啊)。目前还没有想到好的解决办法,如果有同学知道的话还请不吝赐教,这里看一下我的解决方法,在RecyclerView中的onBindViewHolder中调用holder.setIsRecyclable(false) 就可以解决问题啦!

    @Override
    public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
        if (holder instanceof EditVH) {
            /* 强制关闭复用,以解决EditText被复用后长按无法弹出ContextMenu的问题 */
            holder.setIsRecyclable(false);
        } else if (holder instanceof ImgVH) {
            //......
        }
    }

如果是用ListView的话一样可以通过不复用convertView而解决这个问题。

你可能感兴趣的:(Android)