常见的EditText长按菜单如下
oppo
小米
需求是隐藏掉其中的分享/搜索功能,禁止将内容分享到其他应用。
最终解决方案
这里先说下最终解决方案
像华为/oppo等手机,该菜单实际是谷歌系统的即没有改过源代码,像小米的菜单则是自定义,该部分的源代码改动过。
两方面修改:
1.谷歌系统自带的 通过 EditText.setCustomSelectionActionModeCallback()
方法设置自定义的选中后动作模式接口,只保留需要的菜单项
代码如下
editText.customSelectionActionModeCallback = object : ActionMode.Callback { override fun onCreateActionMode( mode: ActionMode?, menu: Menu? ): Boolean { menu?.let { val size = menu.size() for (i in size - 1 downTo 0) { val item = menu.getItem(i) val itemId = item.itemId //只保留需要的菜单项 if (itemId != android.R.id.cut && itemId != android.R.id.copy && itemId != android.R.id.selectAll && itemId != android.R.id.paste ) { menu.removeItem(itemId) } } } return true } override fun onActionItemClicked( mode: ActionMode?, item: MenuItem? ): Boolean { return false } override fun onPrepareActionMode( mode: ActionMode?, menu: Menu? ): Boolean { return false } override fun onDestroyActionMode(mode: ActionMode?) { } }
2.小米等手机自定义菜单无法进行隐藏,可以是分享、搜索等功能失效,即在BaseActivity的startActivityForResult中进行跳转拦截,如果是调用系统的分享/搜索功能,则不允许跳转
override fun startActivityForResult( intent: Intent?, requestCode: Int ) { if (!canStart(intent)) return super.startActivityForResult(intent, requestCode) } @SuppressLint("RestrictedApi") @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) override fun startActivityForResult( intent: Intent?, requestCode: Int, options: Bundle? ) { if (!canStart(intent)) return super.startActivityForResult(intent, requestCode, options) } private fun canStart(intent: Intent?): Boolean { return intent?.let { val action = it.action action != Intent.ACTION_CHOOSER//分享 && action != Intent.ACTION_VIEW//跳转到浏览器 && action != Intent.ACTION_SEARCH//搜索 } ?: false }
如果以上不满足要求,只能通过自定义长按菜单来实现自定义的菜单栏。
解决思路(RTFSC)
分析源码菜单的创建和点击事件
既然是长按松手后弹出的,应该在onTouchEvent中的ACTION_UP事件或者在performLongClick中,从两方面着手
先看perfomLongEvent EditText没有实现 去它的父类TextView中查找
TextView.java public boolean performLongClick() { ···省略部分代码 if (mEditor != null) { handled |= mEditor.performLongClick(handled); mEditor.mIsBeingLongClicked = false; } ···省略部分代码 return handled; }
可看到调用了 mEditor.performLongClick(handled)
方法
Editor.java public boolean performLongClick(boolean handled) { if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) && mInsertionControllerEnabled) { final int offset = mTextView.getOffsetForPosition(mLastDownPositionX, mLastDownPositionY);//获取当前松手时的偏移量 Selection.setSelection((Spannable) mTextView.getText(), offset);//设置选中的内容 getInsertionController().show();//插入控制器展示 mIsInsertionActionModeStartPending = true; handled = true; ··· } if (!handled && mTextActionMode != null) { if (touchPositionIsInSelection()) { startDragAndDrop();//开始拖动 ··· } else { stopTextActionMode(); selectCurrentWordAndStartDrag();//选中当前单词并且开始拖动 ··· } handled = true; } if (!handled) { handled = selectCurrentWordAndStartDrag();//选中当前单词并且开始拖动 ··· } } return handled; }
从上面代码分析
1.长按时会先选中内容 Selection.setSelection((Spannable) mTextView.getText(), offset)
2.显示插入控制器 getInsertionController().show()
3.开始拖动/选中单词后拖动 startDragAndDrop()/ selectCurrentWordAndStartDrag()
看着很像了
看下第二步中展示的内容
Editor.java -> InsertionPointCursorController public void show() { getHandle().show(); if (mSelectionModifierCursorController != null) { mSelectionModifierCursorController.hide(); } } ··· private InsertionHandleView getHandle() { if (mSelectHandleCenter == null) { mSelectHandleCenter = mTextView.getContext().getDrawable( mTextView.mTextSelectHandleRes); } if (mHandle == null) { mHandle = new InsertionHandleView(mSelectHandleCenter); } return mHandle; }
实际是InsertionHandleView 执行了show方法。 查看其父类HandlerView的构造方法
private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) { super(mTextView.getContext()); ··· mContainer = new PopupWindow(mTextView.getContext(), null, com.android.internal.R.attr.textSelectHandleWindowStyle); ··· mContainer.setContentView(this); ··· }
由源码可看出 HandlerView实际上是PopWindow的View。 即选中的图标实际上是popwidow
看源码可看出HandleView有两个实现类 InsertionHandleView 和SelectionHandleView 由名字可看出一个是插入的,一个选择的 看下HandleView的show方法
Editor.java ->HandleView public void show() { if (isShowing()) return; getPositionListener().addSubscriber(this, true ); // Make sure the offset is always considered new, even when focusing at same position mPreviousOffset = -1; positionAtCursorOffset(getCurrentCursorOffset(), false, false); }
看下positionAtCursorOffset方法
Editor.java ->HandleView protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition, boolean fromTouchScreen) { ··· if (offsetChanged || forceUpdatePosition) { if (offsetChanged) { updateSelection(offset); ··· } ··· } }
里面有一个updateSelection更新选中的位置,该方法会导致EditText重绘,再看show方法的getPositionListener().addSubscriber(this, true )
getPositionListener()
返回的实际上是ViewTreeObserver.OnPreDrawListener
的实现类PositionListener
重绘会调用onPreDraw的方法
Editor.java-> PositionListener @Override public boolean onPreDraw() { ··· for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { ··· positionListener.updatePosition(mPositionX, mPositionY, mPositionHasChanged, mScrollHasChanged); ··· } ··· return true; }
调用了positionListener.updatePosition
方法, positionListener这个实现类对应的是HandlerView
重点在HandleView的updatePosition方法,该方法进行popWindow的显示和更新位置
看一下该方法的实现
Editor.java ->HandleView @Override public void updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled) { ··· if (isShowing()) { mContainer.update(pts[0], pts[1], -1, -1); } else { mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]); } } ··· } }
到此我们知道选中的图标即下面红框内的实际上popWindow展示
点击选中的图标可以展示菜单,看下HandleView的onTouchEvent方法
Editor.java ->HandleView @Override public boolean onTouchEvent(MotionEvent ev) { updateFloatingToolbarVisibility(ev); ··· }
updateFloatingToolbarVisibility(ev)
真相在这里,该方法进行悬浮菜单栏的展示
经过进一步查找,可以看到会调用下面SelectionActionModeHelper的这个方法
SelectionActionModeHelper.java public void invalidateActionModeAsync() { cancelAsyncTask(); if (skipTextClassification()) { invalidateActionMode(null); } else { resetTextClassificationHelper(); mTextClassificationAsyncTask = new TextClassificationAsyncTask( mTextView, mTextClassificationHelper.getTimeoutDuration(), mTextClassificationHelper::classifyText, this::invalidateActionMode) .execute(); } }
会启动一个叫TextClassificationAsyncTask的异步任务,该异步任务最后会执行mEditor.getTextActionMode().invalidate()
private void invalidateActionMode(@Nullable SelectionResult result) { ··· final ActionMode actionMode = mEditor.getTextActionMode(); if (actionMode != null) { actionMode.invalidate(); } ··· }
最后看下mTextActionMode 如何在Editor中赋值
Editor.java void startInsertionActionMode() { ··· ActionMode.Callback actionModeCallback = new TextActionModeCallback(false /* hasSelection */); mTextActionMode = mTextView.startActionMode( actionModeCallback, ActionMode.TYPE_FLOATING); ··· }
看下mTextView.startActionMode的注释,在View类中,Start an action mode with the given type. 根据给的类型,开启一个动作模式,该模式是一个TYPE_FLOATING模式,菜单的生成就在TextActionModeCallback类中
在TextActionModeCallback的onCreateActionMode方法中
Editor.java ->TextActionModeCallback @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; } } ··· }
生成的菜单的方法populateMenuWithItems(menu)
中,生成完菜单会执行自定义的回调getCustomCallback()
, 看下该回调如何赋值。
在TextView中
TextView.java public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) { createEditorIfNeeded(); mEditor.mCustomSelectionActionModeCallback = actionModeCallback; }
因此我们可以在自定义回调的onCreateActionMode方法中,删除不需要的菜单项。
但该方法对小米手机无效,小米手机的菜单展示,不是通过startActionMode来展示的。不过可以对菜单中的分享等功能进行禁止跳转,解决方法看最上面
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对自由互联的支持。