Android中TextView文本高亮和点击行为的封装方法
前言
相信大家应该都有所体会,对于一个社交性质的app,业务上少不了给一段文本加上@功能、话题功能,或者是评论上要高亮人名的需求。当然,android为我们提供了clickablespan,用于解决textview部分内容可点击的问题,但却附加了一堆的坑点:
- clickablespan 默认没有高亮行为,也不能添加背景颜色;
- clickablespan 必须配合 movementmethod 使用
- 一旦使用 movementmethod,textview 必定消耗事件
- 当点击clickablespan时,textview的点击也会随后触发
- 当press clickablespan 时, textview的press态也会被触发
这些默认的表现会使得添加 clickablespan 后会出现各种不符合预期的问题,因此我们需要对其进行封装。
据个人使用经验,封装后应该能够方便开发实现以下行为:
- 让span支持字体颜色和背景颜色变化,并且有press态行为
- span的click或者press不影响textview的click和press
- 可选择的决定textview是否应该消耗事件
对于第三点,需要解释下textview是否消耗事件的影响
用一张图来阐述下我们的目的。我们开发过程中,可能将点击事件加在textview上,也可能将点击行为添加在textview的父元素上,例如评论一般是点击整个评论item就可以触发回复。 如果我们把点击事件加在textview的父元素上,那么我们期待的是点击textview的绿色区域应该也要响应点击事件,但现实总是残酷的,如果textview调用了setmovementmethod, 点击绿色区域将不会有任何反应,因为时间被textview消耗了,并不会传递到textview的父元素上。
那我们来一步一步看如何实现这几个问题。
首先我们定义一个接口 itouchablespan, 用于抽象press和点击:
public interface itouchablespan { void setpressed(boolean pressed); void onclick(view widget); }
然后建立一个 clickablespan的子类 qmuitouchablespan 来扩充它的表现:
public abstract class qmuitouchablespan extends clickablespan implements itouchablespan { private boolean mispressed; @colorint private int mnormalbackgroundcolor; @colorint private int mpressedbackgroundcolor; @colorint private int mnormaltextcolor; @colorint private int mpressedtextcolor; private boolean misneedunderline = false; public abstract void onspanclick(view widget); @override public final void onclick(view widget) { if (viewcompat.isattachedtowindow(widget)) { onspanclick(widget); } } public qmuitouchablespan(@colorint int normaltextcolor, @colorint int pressedtextcolor, @colorint int normalbackgroundcolor, @colorint int pressedbackgroundcolor) { mnormaltextcolor = normaltextcolor; mpressedtextcolor = pressedtextcolor; mnormalbackgroundcolor = normalbackgroundcolor; mpressedbackgroundcolor = pressedbackgroundcolor; } // .... get/set ... public void setpressed(boolean isselected) { mispressed = isselected; } public boolean ispressed() { return mispressed; } @override public void updatedrawstate(textpaint ds) { // 通过updatedrawstate来更新字体颜色和背景色 ds.setcolor(mispressed ? mpressedtextcolor : mnormaltextcolor); ds.bgcolor = mispressed ? mpressedbackgroundcolor : mnormalbackgroundcolor; ds.setunderlinetext(misneedunderline); } }
然后我们要把press状态和点击行为传递给qmuitouchablespan,这一层我们可以通过重载 linkmovementmethod去解决:
public class qmuilinktouchmovementmethod extends linkmovementmethod { @override public boolean ontouchevent(textview widget, spannable buffer, motionevent event) { return shelper.ontouchevent(widget, buffer, event) || touch.ontouchevent(widget, buffer, event); } public static movementmethod getinstance() { if (sinstance == null) sinstance = new qmuilinktouchmovementmethod(); return sinstance; } private static qmuilinktouchmovementmethod sinstance; private static qmuilinktouchdecorhelper shelper = new qmuilinktouchdecorhelper(); }
对textview使用 setmovementmethod 后,textview的 ontouchevent 中会调用到 linkmovementmethod的ontouchevent,并且会传入spannable,这是一个去处理spannable数据的好hook点。 我们抽取一个 qmuilinktouchdecorhelper 用于处理公共逻辑,因为linkmovementmethod存在多个行为各异的子类。
public class qmuilinktouchdecorhelper { private itouchablespan mpressedspan; public boolean ontouchevent(textview textview, spannable spannable, motionevent event) { if (event.getaction() == motionevent.action_down) { mpressedspan = getpressedspan(textview, spannable, event); if (mpressedspan != null) { mpressedspan.setpressed(true); selection.setselection(spannable, spannable.getspanstart(mpressedspan), spannable.getspanend(mpressedspan)); } if (textview instanceof qmuispantouchfixtextview) { qmuispantouchfixtextview tv = (qmuispantouchfixtextview) textview; tv.settouchspanhint(mpressedspan != null); } return mpressedspan != null; } else if (event.getaction() == motionevent.action_move) { itouchablespan touchedspan = getpressedspan(textview, spannable, event); if (mpressedspan != null && touchedspan != mpressedspan) { mpressedspan.setpressed(false); mpressedspan = null; selection.removeselection(spannable); } return mpressedspan != null; } else if (event.getaction() == motionevent.action_up) { boolean touchspanhint = false; if (mpressedspan != null) { touchspanhint = true; mpressedspan.setpressed(false); mpressedspan.onclick(textview); } mpressedspan = null; selection.removeselection(spannable); return touchspanhint; } else { if (mpressedspan != null) { mpressedspan.setpressed(false); } selection.removeselection(spannable); return false; } } public itouchablespan getpressedspan(textview textview, spannable spannable, motionevent event) { int x = (int) event.getx(); int y = (int) event.gety(); x -= textview.gettotalpaddingleft(); y -= textview.gettotalpaddingtop(); x += textview.getscrollx(); y += textview.getscrolly(); layout layout = textview.getlayout(); int line = layout.getlineforvertical(y); int off = layout.getoffsetforhorizontal(line, x); itouchablespan[] link = spannable.getspans(off, off, itouchablespan.class); itouchablespan touchedspan = null; if (link.length > 0) { touchedspan = link[0]; } return touchedspan; } }
上述的很多行为直接取自官方的linktouchmovementmethod,然后做了相应的修改。完成这些,我们才仅仅能做到我们想要的第一步而已。
接下来我们看如何处理textview的click与press与 qmuitouchablespan 冲突的问题。 这一步我们需要建立一个textview的子类qmuispantouchfixtextview去处理相关细节。
第一步我们需要判断是否是点击到了qmuitouchablespan, 这个判断可以放在 qmuilinktouchdecorhelper#ontouchevent中完成, 在ontouchevent中补充以下代码:
public boolean ontouchevent(textview textview, spannable spannable, motionevent event) { if (event.getaction() == motionevent.action_down) { // ... if (textview instanceof qmuispantouchfixtextview) { qmuispantouchfixtextview tv = (qmuispantouchfixtextview) textview; tv.settouchspanhint(mpressedspan != null); } return mpressedspan != null; } else if (event.getaction() == motionevent.action_move) { // ... if (textview instanceof qmuispantouchfixtextview) { qmuispantouchfixtextview tv = (qmuispantouchfixtextview) textview; tv.settouchspanhint(mpressedspan != null); } return mpressedspan != null; } else if (event.getaction() == motionevent.action_up) { // ... selection.removeselection(spannable); if (textview instanceof qmuispantouchfixtextview) { qmuispantouchfixtextview tv = (qmuispantouchfixtextview) textview; tv.settouchspanhint(touchspanhint); } return touchspanhint; } else { // ... if (textview instanceof qmuispantouchfixtextview) { qmuispantouchfixtextview tv = (qmuispantouchfixtextview) textview; tv.settouchspanhint(false); } // ... return false; } }
这个时候我们在 qmuispantouchfixtextview就可以通过是否点击到qmuitouchablespan来决定不同行为了,对于点击是非常好处理的,代码如下:
@override public boolean performclick() { if (!mtouchspanhint) { return super.performclick(); } return false; }
对于press行为,就会有点棘手,因为setpress在 ontouchevent多次调用,而且在qmuilinktouchdecorhelper#ontouchevent前就会被调用到,所以不能简单的用mtouchspanhint这个变量来管理。来看看我给出的方案:
// 记录每次真正传入的press,每次更改mtouchspanhint,需要再调用一次setpressed,确保press状态正确 // 第一步: 用一个变量记录setpress传入的值,这个是textview真正的press值 private boolean mispressedrecord = false; // 第二步,ontouchevent在调用super前将mtouchspanhint设为true,这会使得qmuilinktouchdecorhelper#ontouchevent的press行为失效,参考第三步 @override public boolean ontouchevent(motionevent event) { if (!(gettext() instanceof spannable)) { return super.ontouchevent(event); } mtouchspanhint = true; return super.ontouchevent(event); } // 第三步: final掉setpressed,如果!mtouchspanhint才调用super.setpressed,开一个onsetpressed给子类覆写 @override public final void setpressed(boolean pressed) { mispressedrecord = pressed; if (!mtouchspanhint) { onsetpressed(pressed); } } protected void onsetpressed(boolean pressed) { super.setpressed(pressed); } // 第四步: 每次调用settouchspanhint是调用一次setpressed,并传入mispressedrecord,确保press状态的统一 public void settouchspanhint(boolean touchspanhint) { if (mtouchspanhint != touchspanhint) { mtouchspanhint = touchspanhint; setpressed(mispressedrecord); } }
这几个步骤相互耦合,静下心好好理解下。这样就顺利的解决了第二个问题。那么我们来看看如何消除 movementmethod造成textview对事件的消耗行为。
调用 setmovementmethod为何会使得textview必然消耗事件呢?我们可以看看源码:
public final void setmovementmethod(movementmethod movement) { if (mmovement != movement) { mmovement = movement; if (movement != null && !(mtext instanceof spannable)) { settext(mtext); } fixfocusableandclickablesettings(); // selectionmodifiercursorcontroller depends on textcanbeselected, which depends on // mmovement if (meditor != null) meditor.preparecursorcontrollers(); } } private void fixfocusableandclickablesettings() { if (mmovement != null || (meditor != null && meditor.mkeylistener != null)) { setfocusable(true); setclickable(true); setlongclickable(true); } else { setfocusable(false); setclickable(false); setlongclickable(false); } }
原来设置movementmethod后会把clickable,longclickable和focusable都设置为true,这样必然textview会消耗事件了。因此我们想到的解决方案就是:如果我们想不让textview消耗事件,那么我们就在 setmovementmethod之后再改一次clickable,longclickable和focusable。
public void setshouldconsumeevent(boolean shouldconsumeevent) { mshouldconsumeevent = shouldconsumeevent; setfocusable(shouldconsumeevent); setclickable(shouldconsumeevent); setlongclickable(shouldconsumeevent); } public void setmovementmethodcompat(movementmethod movement){ setmovementmethod(movement); if(!mshouldconsumeevent){ setshouldconsumeevent(false); } }
仅仅这样还不够,我们还必须在 ontouchevent里面返回false:
@override public boolean ontouchevent(motionevent event) { if (!(gettext() instanceof spannable)) { return super.ontouchevent(event); } mtouchspanhint = true; // 调用super.ontouchevent,会走到qmuilinktouchmovementmethod // 会走到qmuilinktouchmovementmethod#ontouchevent会修改mtouchspanhint boolean ret = super.ontouchevent(event); if(!mshouldconsumeevent){ return mtouchspanhint; } return ret; }
经过层层fix,我们终于可以给出一份不错的封装代码提供给业务方使用了:
public class qmuispantouchfixtextview extends textview { private boolean mtouchspanhint; // 记录每次真正传入的press,每次更改mtouchspanhint,需要再调用一次setpressed,确保press状态正确 private boolean mispressedrecord = false; private boolean mshouldconsumeevent = true; // textview是否应该消耗事件 public qmuispantouchfixtextview(context context) { this(context, null); } public qmuispantouchfixtextview(context context, attributeset attrs) { this(context, attrs, 0); } public qmuispantouchfixtextview(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); sethighlightcolor(color.transparent); setmovementmethod(qmuilinktouchmovementmethod.getinstance()); } public void setshouldconsumeevent(boolean shouldconsumeevent) { mshouldconsumeevent = shouldconsumeevent; setfocusable(shouldconsumeevent); setclickable(shouldconsumeevent); setlongclickable(shouldconsumeevent); } public void setmovementmethodcompat(movementmethod movement){ setmovementmethod(movement); if(!mshouldconsumeevent){ setshouldconsumeevent(false); } } @override public boolean ontouchevent(motionevent event) { if (!(gettext() instanceof spannable)) { return super.ontouchevent(event); } mtouchspanhint = true; // 调用super.ontouchevent,会走到qmuilinktouchmovementmethod // 会走到qmuilinktouchmovementmethod#ontouchevent会修改mtouchspanhint boolean ret = super.ontouchevent(event); if(!mshouldconsumeevent){ return mtouchspanhint; } return ret; } public void settouchspanhint(boolean touchspanhint) { if (mtouchspanhint != touchspanhint) { mtouchspanhint = touchspanhint; setpressed(mispressedrecord); } } @override public boolean performclick() { if (!mtouchspanhint && mshouldconsumeevent) { return super.performclick(); } return false; } @override public boolean performlongclick() { if (!mtouchspanhint && mshouldconsumeevent) { return super.performlongclick(); } return false; } @override public final void setpressed(boolean pressed) { mispressedrecord = pressed; if (!mtouchspanhint) { onsetpressed(pressed); } } protected void onsetpressed(boolean pressed) { super.setpressed(pressed); } }
总结
以上就是这篇文章的全部内容了,希望本文的内容对给位android开发者们能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。
上一篇: 丹桂飘香是什么季节,丹桂是哪一种桂花!