欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android EditText长按菜单中分享功能的隐藏方法

程序员文章站 2022-06-03 19:15:05
常见的edittext长按菜单如下 oppo 小米 需求是隐藏掉其中的分享/搜索功能,禁止将内容分享到其他应用。 最终解决方案 这里先说下最终...

常见的edittext长按菜单如下

Android EditText长按菜单中分享功能的隐藏方法

oppo

Android EditText长按菜单中分享功能的隐藏方法

小米

需求是隐藏掉其中的分享/搜索功能,禁止将内容分享到其他应用。

最终解决方案

这里先说下最终解决方案

像华为/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来展示的。不过可以对菜单中的分享等功能进行禁止跳转,解决方法看最上面

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。