最近做了一个图文混排的编辑功能,想到了用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而解决这个问题。