常见的edittext长按菜单如下
oppo
小米
需求是隐藏掉其中的分享/搜索功能,禁止将内容分享到其他应用。
最终解决方案
这里先说下最终解决方案
像华为/oppo等手机,该菜单实际是谷歌系统的即没有改过源代码,像小米的菜单则是自定义,该部分的源代码改动过。
两方面修改:
1.谷歌系统自带的 通过 edittext.setcustomselectionactionmodecallback()
方法设置自定义的选中后动作模式接口,只保留需要的菜单项
代码如下
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
37
38
39
40
|
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中进行跳转拦截,如果是调用系统的分享/搜索功能,则不允许跳转
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
|
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中查找
1
2
3
4
5
6
7
8
9
10
11
|
textview.java public boolean performlongclick() { ···省略部分代码 if (meditor != null ) { handled |= meditor.performlongclick(handled); meditor.misbeinglongclicked = false ; } ···省略部分代码 return handled; } |
可看到调用了 meditor.performlongclick(handled)
方法
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
|
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()
看着很像了
看下第二步中展示的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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的构造方法
1
2
3
4
5
6
7
8
9
|
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方法
1
2
3
4
5
6
7
8
9
|
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方法
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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的显示和更新位置
看一下该方法的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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方法
1
2
3
4
5
6
|
editor.java ->handleview @override public boolean ontouchevent(motionevent ev) { updatefloatingtoolbarvisibility(ev); ··· } |
updatefloatingtoolbarvisibility(ev)
真相在这里,该方法进行悬浮菜单栏的展示
经过进一步查找,可以看到会调用下面selectionactionmodehelper的这个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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()
1
2
3
4
5
6
7
8
|
private void invalidateactionmode( @nullable selectionresult result) { ··· final actionmode actionmode = meditor.gettextactionmode(); if (actionmode != null ) { actionmode.invalidate(); } ··· } |
最后看下mtextactionmode 如何在editor中赋值
1
2
3
4
5
6
7
8
9
10
|
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方法中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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中
1
2
3
4
5
|
textview.java public void setcustomselectionactionmodecallback(actionmode.callback actionmodecallback) { createeditorifneeded(); meditor.mcustomselectionactionmodecallback = actionmodecallback; } |
因此我们可以在自定义回调的oncreateactionmode方法中,删除不需要的菜单项。
但该方法对小米手机无效,小米手机的菜单展示,不是通过startactionmode来展示的。不过可以对菜单中的分享等功能进行禁止跳转,解决方法看最上面
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。
原文链接:https://juejin.im/post/5c720ef6f265da2d943f6989