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

常見的EditText長按菜單以下 java

oppo長按菜單
oppo
小米長按菜單
小米

需求是隱藏掉其中的分享/搜索功能,禁止將內容分享到其餘應用。android

最終解決方案

這裏先說下最終解決方案
像華爲/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中進行跳轉攔截,若是是調用系統的分享/搜索功能,則不容許跳轉bash

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)

分析源碼菜單的建立和點擊事件ide

既然是長按鬆手後彈出的,應該在onTouchEvent中的ACTION_UP事件或者在performLongClick中,從兩方面着手
先看perfomLongEvent EditText沒有實現 去它的父類TextView中查找ui

TextView.java
    public boolean performLongClick() {
       ···省略部分代碼
        if (mEditor != null) {
            handled |= mEditor.performLongClick(handled);
            mEditor.mIsBeingLongClicked = false;
        }

       ···省略部分代碼
        return handled;
    }
複製代碼

可看到調用了 mEditor.performLongClick(handled)方法this

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()
看着很像了
看下第二步中展現的內容spa

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的構造方法code

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來展現的。不過能夠對菜單中的分享等功能進行禁止跳轉,解決方法看最上面

相關文章
相關標籤/搜索