使用代码调出 TextView/EditText 的编辑菜单

Posted by Vove on December 12, 2019

0x0

大家都知道在输入框长按文字,会出现编辑菜单。最近遇到一个需求:代码直接调出 EditText(TextView 需要设置 setTextIsSelectable(true)) 的编辑菜单,这里我叫它 EditorActionMenu

既然通过长按可以调出,为何不直接 EditText.performLongClick()View.showContextMenu() 方法。事实证明,此代码无法调出 EditorActionMenu,下面进行分析如何弹出编辑菜单。

过程分析

编辑菜单弹出过程: 按下 -> 等待 -> 松开 -> 弹出菜单

对应 ViewTouch 事件: ACTION_DOWN => performLongClick => ACTION_UP

由于 TextView 拦截了 onTouchEventonTouchEventperformLongClick 源码结合长按过程和代码调试可以分析出真正显示菜单的代码执行过程: mEditor.onTouchEvent(event) => mEditor.performLongClick(handled); => mEditor.startInsertionActionMode(); =>

TextView onTouchEventperformLongClick 源码(省略部分代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 	@Override
    public boolean performLongClick() {
   		//.....
        if (mEditor != null) {
        	//长按事件
            handled |= mEditor.performLongClick(handled);
            mEditor.mIsBeingLongClicked = false;
        }
        //....
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getActionMasked();
        if (mEditor != null) {
            mEditor.onTouchEvent(event);
            if (mEditor.mSelectionModifierCursorController != null
                    && mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
                return true;
            }
        }
        final boolean superResult = super.onTouchEvent(event);
		// ACTION_UP 后执行
        if (mEditor != null && mEditor.mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
            mEditor.mDiscardNextActionUp = false;

            if (mEditor.mIsInsertionActionModeStartPending) {
                mEditor.startInsertionActionMode();
                mEditor.mIsInsertionActionModeStartPending = false;
            }
            return superResult;
        }
        //.......
        //.......
        return superResult;
    }

实现

反射实现

根据上面分析,菜单的弹出由 Editor 类控制,但这个类不对外开放 (被@hide标注) 在开发中无法接触到这个类。利用反射可以实现,但考虑反射可能带来出乎意料的情况,并且 Android P 已禁止利用反射进行这种操作,这里就不考虑了。

模拟实现

利用代码模拟 TouchEvent 来模拟手指动作。

这里使用 KotlinTextView 扩展一个 showEditorActionMenu 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun TextView.showEditorActionMenu() {
    //获取焦点
    requestFocus()
    //按下坐标
    val x: Float = (width / 2).toFloat()
    val y: Float = (height / 2).toFloat()
    // 按下事件
    onTouchEvent(newMotionEvent(MotionEvent.ACTION_DOWN, x, y))
    //延时发送松开事件
    postDelayed({
       onTouchEvent(newMotionEvent(MotionEvent.ACTION_UP, x, y))
    }, ViewConfiguration.getLongPressTimeout().toLong())
}
private fun newMotionEvent(action: Int, x: Float, y: Float): MotionEvent {
    //无需考虑 按下时间和事件时间
    return MotionEvent.obtain(0, 0, action, x, y, 0)
}

不过上面有个缺点, postDelayed 造成等待 LongPressTimeout 时间后才显示菜单。

优化模拟

不使用延时,直接调用 ACTION_DOWN => performLongClick => ACTION_UP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun TextView.showEditorActionMenu() {
    //获取焦点
    requestFocus()
    //按下坐标
    val x: Float = (width / 2).toFloat()
    val y: Float = (height / 2).toFloat()
    // 按下事件
    onTouchEvent(newMotionEvent(MotionEvent.ACTION_DOWN, x, y))
    //长按事件
    performLongClick()
    //发送松开事件
    onTouchEvent(newMotionEvent(MotionEvent.ACTION_UP, x, y))
}
private fun newMotionEvent(action: Int, x: Float, y: Float): MotionEvent {
    //无需考虑 按下时间和事件时间
    return MotionEvent.obtain(0, 0, action, x, y, 0)
}

速度相比优化前稍微快些