需求描述:这是一个类似新浪微博发帖加话题/@好友功能中的一个小交互,不是如何实现话题功能,而是讲一下点击Span文本选中的交互实现,点击Span选中和EditText的点击、长按事件冲突如何解决。
实现过程:
咋一看好像这个交互也太简单了,类似这样
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
LogUtils.e("-->>点击了话题");
widget.post(new Runnable() {
@Override
public void run() {
// 这里不能使用 start 和 end,因为选中再进行输入操作时,有可能会改变range的的范围
setSelection(range.getFrom(), range.getTo());
LogUtils.e("-->>选中 start=" + start + ",end=" + end);
}
});
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
ds.setUnderlineText(false);
}
};
builder.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
richEt.setText(builder);
richEt.setMovementMethod(LinkMovementMethod.getInstance());
运行起来,你就会发现一堆坑,比如,ClickableSpan 与Edittext自身的点击事件一起触发等等。
网上搜索了一波发现,太多大佬遇到这个问题了,解决方式也很成熟,代码如下
public class LinkTouchMethod implements View.OnTouchListener {
long longClickDelay = ViewConfiguration.getLongPressTimeout();
long startTime = 0;
/**
* 需要判断一下,如果触发了
*
* @param v
* @param event
* @return
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
startTime = System.currentTimeMillis();
}
TextView tv = (TextView) v;
CharSequence text = tv.getText();
if (text instanceof Spanned) {
if (action == MotionEvent.ACTION_UP) {
// 避免长按和点击冲突,如果超过400毫秒,认为是在长按,不执行点击操作
if (System.currentTimeMillis() - startTime > longClickDelay) {
LogUtils.e("-->>认为是长按");
return false;
}
int x = (int) event.getX();
int y = (int) event.getY();
x -= tv.getTotalPaddingLeft();
y -= tv.getTotalPaddingTop();
x += tv.getScrollX();
y += tv.getScrollY();
LogUtils.e("-->>y=" + y + " ," + event.toString());
Layout layout = tv.getLayout();
// 获取y坐标所在行数
int line = layout.getLineForVertical(y);
LogUtils.e("-->>line=" + line);
// 获取所在行数 x坐标的偏移量
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = ((Spanned) text).getSpans(off, off, ClickableSpan.class);
if (link.length != 0) {
if (x < layout.getLineWidth(line) && x > 0) {
link[0].onClick(tv);
// 需要拦截view本身的点击事件
return true;
}
}
}
}
LogUtils.e("-->>没有处理onTouch " + action);
return false;
}
}
// 设置触摸监听
richEt.setOnTouchListener(new LinkTouchMethod());
这里复写了一个OnTouchListener ,然后给EditText添加触摸监听。
跑起来,发现好像也点击选中,不过让人吐血的是,EditText的长按监听也触发了。搜索了半天没找到原因,迫不得已去看源码,最后在事件分发的源码里面发现了问题。
public boolean dispatchTouchEvent(MotionEvent event) {
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
...
}
当我们在LinkTouchMethod的onTouch里面对up事件返回true进行消费时,这里result会被赋值为true,根本就不会走到onTouchEvent里面,不管是点击还是长按、长按取消都是在onTouchEvent里面进行的,所以长按没有被取消,就会在发出Span点击的时候也会触发长按监听。
到了这里ClickableSpan实现不了,OnTouchListener也不能用,没辙,只能保存up事件,自己在EditText的点击事件里面去进行span的点击触发,根据条件选择触发SpanClick或者EditText的onClick。
可以实现GestureDetector在onSingleTapUp中捕获up事件,或者重写EditText在onTouchEvent中保存up事件,我项目里使用复写onTouchEvent比较好。
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_UP) {
// 必须保存MotionEvent副本,因为这个会被回收
this.upMotionEvent = MotionEvent.obtain(event);
}
/*if (gestureDetector != null) {
gestureDetector.onTouchEvent(event);
}*/
return super.onTouchEvent(event);
}
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 正常点击事件
.......
// 保存的up事件
if (upMotionEvent != null) {
// 通过up事件去匹配span,并响应点击
SpanClickHelper.spanClickHandle(MentionEditText.this, upMotionEvent);
upMotionEvent = null;
}
}
});
这里也有个坑,MotionEvent分发使用完会被回收,尼玛我说怎么好多点击匹配不到Span。
最后,点击、长按都可以选中Span文本。
收工,看msi去了!
demo地址:Study_Demo/Study_RichText at master · zombiu/Study_Demo (github.com)
感谢:
Android仿微博@好友,#话题#及links处理方案_BoBoMEe-CSDN博客
安卓 TextView 七宗罪_陈蒙的博客-CSDN博客
https://yangqiuyan.github.io/2018/11/21/LinkMovementMethod/
网友评论