Android隱藏EditText長按選單中分享功能探索

焦世春發表於2019-02-24

常見的EditText長按選單如下

oppo長按選單
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展示

Android隱藏EditText長按選單中分享功能探索

點選選中的圖示可以展示選單,看下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來展示的。不過可以對選單中的分享等功能進行禁止跳轉,解決方法看最上面

相關文章