天天看點

RecyclerView複用EditText後長按不可選中

最近做了一個圖文混排的編輯功能,想到了用RecyclerView設定不同的ViewType,實作EditText和ImageView的混排效果。如圖:

RecyclerView複用EditText後長按不可選中

但有一個問題困擾了我很久,就是編輯少量内容的時候正常,當編輯的内容多了,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而解決這個問題。