Android隱藏EditText長按選單中分享功能探索
常見的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來展示的。不過可以對選單中的分享等功能進行禁止跳轉,解決方法看最上面