最近做了一個圖文混排的編輯功能,想到了用RecyclerView設定不同的ViewType,實作EditText和ImageView的混排效果。如圖:
但有一個問題困擾了我很久,就是編輯少量内容的時候正常,當編輯的内容多了,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而解決這個問題。