EditText是如何实现长按弹出复制粘贴等ContextMenu的源码解析

2024-02-28 09:18

本文主要是介绍EditText是如何实现长按弹出复制粘贴等ContextMenu的源码解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

最近在做一些关于EditText编辑功能的需求,遇到了很多的问题,比如EditText在RecyclerView中会出现内容错乱、RecyclerView复用EditText后长按无法弹出复制、粘贴、全选ContextMenu等一些问题,在网上也没有搜到比较好的解决方法,于是就想研究一下这方面的源码,希望能帮到有需要的同学,少走一些弯路。 
网上看到的关于EditText的ContextMenu的问题,大部分是如何屏蔽长按后不弹,如何自定义ContextMenu的需求,本篇文章介绍Android系统是如何实现长按EditText弹出ContextMenu的,如果原理都明白了,那问题还不迎刃而解嘛,废话不多说,先看一个效果图: 

非常常见的功能,要研究这个功能的实现该从哪入手呢,我说一下我的思路:从EditText的长按事件开始,翻看EditText的源码,发现内容很少,并没有事件处理方法,于是找到了父View(TextView), TextView中有一个方法叫performLongClick,没错,就是它:

@Overridepublic boolean performLongClick() {boolean handled = false;if (mEditor != null) {mEditor.mIsBeingLongClicked = true;}//执行父view的performLongClickif (super.performLongClick()) {handled = true;}//执行mEditor的performLongClickif (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:

@Overridepublic 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);//弹出ContextMenuif (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 selectionif (!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() {//获取选中效果左边的Drawableif (mSelectHandleLeft == null) {mSelectHandleLeft = mTextView.getContext().getDrawable(mTextView.mTextSelectHandleLeftRes);}//获取选中效果右边的Drawableif (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);}//显示两边的DrawablemStartHandle.show();mEndHandle.show();hideInsertionPointCursorController();}

原来这个方法是显示选中文字的两边光标效果的,并没有看到我们预期的结果,烦躁啊,但通过这个方法我们可以看到,如果我们想改变这个光标的话,只需要修改mTextView.mTextSelectHandleLeftRes和mTextView.mTextSelectHandleRightRes就行了,这两个属性想必在TextView的xml里面可以直接设置,也算是有一点点收获吧,至少现在文字已经选中了。 
又经过了很长时间的debug,发现这个menu并没有在performLongClick()方法里实现,而是在onTouchEvent()方法中,当事件为ACTION_UP的时候。

@Overridepublic 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有选中,那么启动选中actionModeif (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 {//......@Overridepublic 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 {//这里会创建一个FloatingActionModemode = createActionMode(type, wrappedCallback, originatingView);//调用TextActionModeCallback 类的onCreateActionMode方法创建menuif (mode != null && wrappedCallback.onCreateActionMode(mode, mode.getMenu())) {//处理ActionModesetHandledActionMode(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;//创建FloatingToolbarmFloatingToolbar = new FloatingToolbar(mContext, mWindow);((FloatingActionMode) mFloatingActionMode).setFloatingToolbar(mFloatingToolbar);//显示FloatingToolbarmFloatingActionMode.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。

这篇关于EditText是如何实现长按弹出复制粘贴等ContextMenu的源码解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/755055

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

禁止平板,iPad长按弹出默认菜单事件

通过监控按下抬起时间差来禁止弹出事件,把以下代码写在要禁止的页面的页面加载事件里面即可     var date;document.addEventListener('touchstart', event => {date = new Date().getTime();});document.addEventListener('touchend', event => {if (new

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P