EditText是如何实现长按弹出复制粘贴等ContextMenu的源码解析
来源:互联网 发布:人工智能开发语言 编辑:程序博客网 时间:2024/05/21 15:44
最近在做一些关于EditText编辑功能的需求,遇到了很多的问题,比如EditText在RecyclerView中会出现内容错乱、RecyclerView复用EditText后长按无法弹出复制、粘贴、全选ContextMenu等一些问题,在网上也没有搜到比较好的解决方法,于是就想研究一下这方面的源码,希望能帮到有需要的同学,少走一些弯路。
网上看到的关于EditText的ContextMenu的问题,大部分是如何屏蔽长按后不弹,如何自定义ContextMenu的需求,本篇文章介绍Android系统是如何实现长按EditText弹出ContextMenu的,如果原理都明白了,那问题还不迎刃而解嘛,废话不多说,先看一个效果图:
非常常见的功能,要研究这个功能的实现该从哪入手呢,我说一下我的思路:从EditText的长按事件开始,翻看EditText的源码,发现内容很少,并没有事件处理方法,于是找到了父View(TextView), TextView中有一个方法叫performLongClick,没错,就是它:
@Override public boolean performLongClick() { boolean handled = false; if (mEditor != null) { mEditor.mIsBeingLongClicked = true; } //执行父view的performLongClick if (super.performLongClick()) { handled = true; } //执行mEditor的performLongClick if (mEditor != null) { handled |= mEditor.performLongClick(handled); mEditor.mIsBeingLongClicked = false; } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); if (mEditor != null) mEditor.mDiscardNextActionUp = true; } return handled; }
我们发现调用了super.performLongClick(),然后再到View中去看:
private boolean performLongClickInternal(float x, float y) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); boolean handled = false; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnLongClickListener != null) { handled = li.mOnLongClickListener.onLongClick(View.this); } if (!handled) { final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y); handled = isAnchored ? showContextMenu(x, y) : showContextMenu(); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; }
大家看这一句handled = isAnchored ? showContextMenu(x, y) : showContextMenu(); 感觉就要接近真相了,赶紧点进去:
public boolean showContextMenu(float x, float y) { return getParent().showContextMenuForChild(this, x, y); }
这里面调的是父View的showContextMenuForChild方法,不同的页面父View都不同,一般都是LinearLayout、RelativeLayout,但他们没有重写这个方法,都用的ViewGroup的showContextMenuForChild:
@Override public boolean showContextMenuForChild(View originalView, float x, float y) { try { mGroupFlags |= FLAG_SHOW_CONTEXT_MENU_WITH_COORDS; if (showContextMenuForChild(originalView)) { return true; } } finally { mGroupFlags &= ~FLAG_SHOW_CONTEXT_MENU_WITH_COORDS; } return mParent != null && mParent.showContextMenuForChild(originalView, x, y); }
debug会发现这个方法会一直向上找父View,直到DecorView。DecorView中实际调用了showContextMenuForChildInternal方法:
private boolean showContextMenuForChildInternal(View originalView, float x, float y) { //..... final MenuHelper helper; final boolean isPopup = !Float.isNaN(x) && !Float.isNaN(y); //弹出ContextMenu if (isPopup) { helper = mWindow.mContextMenu.showPopup(getContext(), originalView, x, y); } else { helper = mWindow.mContextMenu.showDialog(originalView, originalView.getWindowToken()); } if (helper != null) { // If it's a dialog, the callback needs to handle showing // sub-menus. Either way, the callback is required for propagating // selection to Context.onContextMenuItemSelected(). callback.setShowDialogForSubmenu(!isPopup); helper.setPresenterCallback(callback); } mWindow.mContextMenuHelper = helper; return helper != null; }
最关键的一句话helper = mWindow.mContextMenu.showPopup(getContext(), originalView, x, y);
看到这里本以为真相大白了,遗憾的是debug看这句话返回的helper为null,也就是并没有执行预期的复制、粘贴menu的显示,从showPopup这个方法里面可以看出,这个方法要做的事情就是我们View里面或者是activity里面弹出的ContextMenu,比如,微信的聊天列表,长按弹出的popupWindow就是通过这个方法实现的,这方面的问题百度有很多文章。
现在线索突然断了,好烦躁,需要静下心来好好思考一下,既然这条路走不通,那肯定有别的途径,还记得前边performLongClick()方法吗?handled |= mEditor.performLongClick(handled); 看到没有,想必就是它了,瞬间精神了许多:
public boolean performLongClick(boolean handled) { // .....省略了无关代码 // Start a new selection if (!handled) { handled = selectCurrentWordAndStartDrag(); } return handled; }
看到selectCurrentWordAndStartDrag()这个方法心里就放心了,从字面上能看出是选中当前文字然后开始拖拽。
/** * If the TextView allows text selection, selects the current word when no existing selection * was available and starts a drag. * * @return true if the drag was started. */ private boolean selectCurrentWordAndStartDrag() { //...... if (!checkField()) { return false; } //如果mTextView没有选中,那么选中当前文字 if (!mTextView.hasSelection() && !selectCurrentWord()) { // No selection and cannot select a word. return false; } stopTextActionModeWithPreservingSelection(); getSelectionController().enterDrag( SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD); return true; }
这个方法的注释意思是如果当前TextView允许选中,那么选中当前文字然后开启拖拽效果,selectCurrentWord()方法中关键的一句是Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); 可以看出这个地方就是选中当前文字的实现。enterDrag呢?getSelectionController()得到的是SelectionModifierCursorController,这个类从字面上看是选中、修改光标的控制器,
public void enterDrag(int dragAcceleratorMode) { // Just need to init the handles / hide insertion cursor. show(); mDragAcceleratorMode = dragAcceleratorMode; // Start location of selection. mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX, mLastDownPositionY); mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY); // Don't show the handles until user has lifted finger. hide(); // ...... }
我们看到show()这个方法,很有可能就是显示menu的实现:
public void show() { if (mTextView.isInBatchEditMode()) { return; } initDrawables(); initHandles(); }
里面有两个方法,分别看一下:
private void initDrawables() { //获取选中效果左边的Drawable if (mSelectHandleLeft == null) { mSelectHandleLeft = mTextView.getContext().getDrawable( mTextView.mTextSelectHandleLeftRes); } //获取选中效果右边的Drawable if (mSelectHandleRight == null) { mSelectHandleRight = mTextView.getContext().getDrawable( mTextView.mTextSelectHandleRightRes); } } private void initHandles() { // Lazy object creation has to be done before updatePosition() is called. //将选中效果左右两边的Drawable以SelectionHandleView的形式创建出来 if (mStartHandle == null) { mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight, com.android.internal.R.id.selection_start_handle, HANDLE_TYPE_SELECTION_START); } if (mEndHandle == null) { mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft, com.android.internal.R.id.selection_end_handle, HANDLE_TYPE_SELECTION_END); } //显示两边的Drawable mStartHandle.show(); mEndHandle.show(); hideInsertionPointCursorController(); }
原来这个方法是显示选中文字的两边光标效果的,并没有看到我们预期的结果,烦躁啊,但通过这个方法我们可以看到,如果我们想改变这个光标的话,只需要修改mTextView.mTextSelectHandleLeftRes和mTextView.mTextSelectHandleRightRes就行了,这两个属性想必在TextView的xml里面可以直接设置,也算是有一点点收获吧,至少现在文字已经选中了。
又经过了很长时间的debug,发现这个menu并没有在performLongClick()方法里实现,而是在onTouchEvent()方法中,当事件为ACTION_UP的时候。
@Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); if (mEditor != null) { mEditor.onTouchEvent(event); if (mEditor.mSelectionModifierCursorController != null && mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) { return true; } } //........ }
又是mEditor ,看来这个类真的很重要啊,进入onTouchEvent方法
void onTouchEvent(MotionEvent event) { //...... if (hasSelectionController()) { getSelectionController().onTouchEvent(event); } //...... }
关键的只有这一句,getSelectionController()我们上边已经看到过了,返回的是SelectionModifierCursorController,然后我们看看里面的onTouchEvent方法:
public void onTouchEvent(MotionEvent event) { //...... switch (event.getActionMasked()) { //...... case MotionEvent.ACTION_UP: if (!isDragAcceleratorActive()) { break; } updateSelection(event); // No longer dragging to select text, let the parent intercept events. mTextView.getParent().requestDisallowInterceptTouchEvent(false); // No longer the first dragging motion, reset. resetDragAcceleratorState(); //如果mTextView有选中,那么启动选中actionMode if (mTextView.hasSelection()) { startSelectionActionMode(); } break; } }
直接进入MotionEvent.ACTION_UP事件,mTextView.hasSelection()想必肯定是true,以为前面的performLongClick()分析,已经处于选中状态了,赶紧看看这个方法吧:
boolean startSelectionActionMode() { boolean selectionStarted = startSelectionActionModeInternal(); if (selectionStarted) { getSelectionController().show(); } mRestartActionModeOnNextRefresh = false; return selectionStarted; }
实际上调用的是startSelectionActionModeInternal()方法,真相已经渐渐浮出水面了,
private boolean startSelectionActionModeInternal() { //...... ActionMode.Callback actionModeCallback = new TextActionModeCallback(true /* hasSelection */); mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING); //...... return selectionStarted; }
关键的地方到了,这里实例化了一个TextActionModeCallback对象,我们看看这个类的实现:
/** * An ActionMode Callback class that is used to provide actions while in text insertion or * selection mode. * * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace * actions, depending on which of these this TextView supports and the current selection. */ private class TextActionModeCallback extends ActionMode.Callback2 { //...... @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { mode.setTitle(null); mode.setSubtitle(null); mode.setTitleOptionalHint(true); populateMenuWithItems(menu); Callback customCallback = getCustomCallback(); if (customCallback != null) { if (!customCallback.onCreateActionMode(mode, menu)) { // The custom mode can choose to cancel the action mode, dismiss selection. Selection.setSelection((Spannable) mTextView.getText(), mTextView.getSelectionEnd()); return false; } } if (mTextView.canProcessText()) { mProcessTextIntentActionsHandler.onInitializeMenu(menu); } if (menu.hasVisibleItems() || mode.getCustomView() != null) { if (mHasSelection && !mTextView.hasTransientState()) { mTextView.setHasTransientState(true); } return true; } else { return false; } } }
看一下这个类的注释,这是一个用来提供文本的插入、选中等操作的回调,默认的回调提供了全选、剪切、复制、粘贴、分享和替换,真的就是它了,
这里需要提一嘴的是Callback customCallback = getCustomCallback();这一句表示开发者可以自己实现menu的创建,通过TextView的setCustomSelectionActionModeCallback()方法设置的,如果大家有这个需求的话可以在这个地方尝试(我没试过)。
默认的menu是通过populateMenuWithItems(menu);这句话实现的:
private void populateMenuWithItems(Menu menu) { if (mTextView.canCut()) { menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT, com.android.internal.R.string.cut). setAlphabeticShortcut('x'). setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); } if (mTextView.canCopy()) { menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY, com.android.internal.R.string.copy). setAlphabeticShortcut('c'). setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); } if (mTextView.canPaste()) { menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE, com.android.internal.R.string.paste). setAlphabeticShortcut('v'). setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); } if (mTextView.canShare()) { menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE, com.android.internal.R.string.share). setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); } updateSelectAllItem(menu); updateReplaceItem(menu); }
现在实现menu的地方已经找到了,那么在什么时候会调用onCreateActionMode呢?什么时候回显示呢?我们继续看看前边的startSelectionActionModeInternal()方法,下边一句是mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);这个方法有两个参数,第一个参数就是我们刚刚分析的TextActionModeCallback 回调,是menu的实现,第二个参数是ActionMode.TYPE_FLOATING,应该是在显示menu的时候用到的,floating嘛,对不对,继续跟进:
public ActionMode startActionMode(ActionMode.Callback callback, int type) { ViewParent parent = getParent(); if (parent == null) return null; try { return parent.startActionModeForChild(this, callback, type); } catch (AbstractMethodError ame) { // Older implementations of custom views might not implement this. return parent.startActionModeForChild(this, callback); } }
这里面会一直调用parent.startActionModeForChild,一直到DecorView,最终会调用到DecorView的startActionMode()方法:
private ActionMode startActionMode( View originatingView, ActionMode.Callback callback, int type) { //将callback包装到wrappedCallback ActionMode.Callback2 wrappedCallback = new ActionModeCallback2Wrapper(callback); ActionMode mode = null; //...... if (mode != null) { if (mode.getType() == ActionMode.TYPE_PRIMARY) { //...... } else { //这里会创建一个FloatingActionMode mode = createActionMode(type, wrappedCallback, originatingView); //调用TextActionModeCallback 类的onCreateActionMode方法创建menu if (mode != null && wrappedCallback.onCreateActionMode(mode, mode.getMenu())) { //处理ActionMode setHandledActionMode(mode); } else { mode = null; } }//...... return mode; }
这里面我们看到了创建menu的调用代码,想必setHandledActionMode(mode);这个方法会将它show出来:
private void setHandledActionMode(ActionMode mode) { if (mode.getType() == ActionMode.TYPE_PRIMARY) { setHandledPrimaryActionMode(mode); } else if (mode.getType() == ActionMode.TYPE_FLOATING) { setHandledFloatingActionMode(mode); } }
这里看到了前面提到的ActionMode.TYPE_FLOATING的作用了,继续看:
private void setHandledFloatingActionMode(ActionMode mode) { mFloatingActionMode = mode; //创建FloatingToolbar mFloatingToolbar = new FloatingToolbar(mContext, mWindow); ((FloatingActionMode) mFloatingActionMode).setFloatingToolbar(mFloatingToolbar); //显示FloatingToolbar mFloatingActionMode.invalidate(); // Will show the floating toolbar if necessary. mFloatingActionModeOriginatingView.getViewTreeObserver() .addOnPreDrawListener(mFloatingToolbarPreDrawListener); }
到这里就终于结束了,menu最终以FloatingToolbar的形式显示出来
总结一下
1、EditText(或者说TextView)长按选中的效果是在Editor.performLongClick(handled)中实现的,这个方法会让当前文本处于选中状态,并显示选中的左右Drawable
2、长按弹出的复制、全选、粘贴等menu的显示过程:首先是Editor内部类SelectionModifierCursorController在onTouchEvent处理MotionEvent.ACTION_UP事件,然后调用startSelectionActionModeInternal()方法,并创建了TextActionModeCallback用来初始化menuItem,然后通过TextView的startActionMode一直往上找,最终由DecorView以FloatingToolbar的形式展现出来。
3、如果我们想自定义menu有哪些item,可以通过TextView的setCustomSelectionActionModeCallback实现,可以参照TextActionModeCallback。
4、如果有别的需求或者是issue,大家可以通过这个流程来想办法。
文章很长,感谢各位同学能耐心的看到这,如果文中有不对的地方,还请多多指正。谢谢!!
- EditText是如何实现长按弹出复制粘贴等ContextMenu的源码解析
- 类似于 QQ长按弹出菜单视图 (主要是文字的复制、粘贴)
- WebView长按弹出复制粘贴
- 自定义EditText 的复制 、粘贴、剪切等
- EditView与TextView如何实现长按复制、粘贴、选择
- 长按edittext 复制图片该如何实现
- android开发之长按弹出粘贴,点击之后复制
- 安卓5.1文本框屏蔽长按弹出的复制粘贴
- 监听EditText的复制、粘贴、全选、剪切、选择等状态
- EditText 屏蔽选择、复制、粘贴等一切剪切板的操作
- android 监听EditText复制粘贴等操作
- android EditText 监听复制粘贴等操作
- ListView:长按弹出上下文菜单(ContextMenu)
- android EditText 屏蔽长按弹出剪切 复制 全选菜单 的解决办法
- EditText 长按弹出的上下文菜单(如何修改系统默认弹出的上下文菜单)
- TextView的长按复制(高仿微信文字复制粘贴)
- TextView长按复制-粘贴
- html5+CSS 禁止IOS长按复制粘贴实现
- 翻车事故分析专栏
- 基于SSM的汽车租赁系统
- 仿人机器人的跑步研究学习笔记1之机器人的基础知识
- 非旋转 Treap 学习笔记(一)
- 让 ul 里面的li居中
- EditText是如何实现长按弹出复制粘贴等ContextMenu的源码解析
- 算法时间复杂度分析
- 全局js的使用。
- 关于阿里云服务器上连接的问题
- ui
- MySQL数据库PDO教程
- MySQL 5.5.X版本GROUP BY错误解决方法
- 数据结构基本概念
- 1004