给TextView设置了OnLongClickListener后,如果TextView中同时有ClickableSpan(一般为URLSpan),此时长按TextView,如果长按的位置在ClickableSpan上,会发现同时触发了OnLongClickListener和ClickableSpan的onClick。比如下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| final TextView tv = (TextView) findViewById(R.id.txt); String url = "http://www.baidu.com"; URLSpan span = new URLSpan(url); String content = url + "Test"; SpannableString ss = new SpannableString(content); ss.setSpan(span, 0, url.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); tv.setText(ss); tv.setMovementMethod(LinkMovementMethod.getInstance()); tv.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { Toast.makeText(getApplicationContext(), "OnLongClick", Toast.LENGTH_SHORT).show(); return true; } });
|
当在Url上长按时,会同时弹出Toast并打开指定的网页。为什么这样?当然还要从源码中查找,看下TextView的onTouchEvent函数。
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
| @Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); if (mEditor != null) mEditor.onTouchEvent(event); final boolean superResult = super.onTouchEvent(event); if (mEditor != null && mEditor.mDiscardNextActionUp && action == MotionEvent.ACTION_UP) { mEditor.mDiscardNextActionUp = false; return superResult; } final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused(); if ((mMovement != null || onCheckIsTextEditor()) && isEnabled() && mText instanceof Spannable && mLayout != null) { boolean handled = false; if (mMovement != null) { handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); } ... if (handled) { return true; } } return superResult; }
|
在上面标注的第一步中,调用super.onTouchEvent
,会处理长按事件。在接下来的第二步中,判断mMovement
是否等于null
,如果不为null
,无论第一步中返回的结果是true还是false,都会调用mMovement
的onTouchEvent
,Shit,问题就在这里了。而LinkMovementMethod
的onTouchEvent
是这样的
1 2 3 4 5 6
| if (action == MotionEvent.ACTION_UP) { link[0].onClick(widget); } else if (action == MotionEvent.ACTION_DOWN) { Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0])); }
|
省略了次要部分,在ACTION_UP
时就调用link
即ClickableSpan
的onClick了,Holy Shit。
问题找到了,那么怎么解决?从上面代码中看到,在进入第二步前有一个很长的if
条件判断,破坏这个条件就行了,所以可以在OnLongClickListner
中setEnabled(false)
,在ACTION_UP
中再setEnabled(true)
,但什么时候调用setEnabled(true)
?可以设置OnTouchListener
,在ACTION_UP
时post一个Runnable
来设置,不过个人不喜欢这个方法。
另一个办法比较取巧,你要调用onClick就调用吧,我判断一下如果触发了长按就在onClick中什么都不做不就行了,这就要求继承ClickableSpan
。那么判断的方法呢?最简单的方法就是通过View.setTag
,如下所示
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
| tv.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { Toast.makeText(getApplicationContext(), "onLongClick", Toast.LENGTH_SHORT).show(); v.setTag("Test"); return true; } }); private static class NonLongClickableUrlSpan extends URLSpan { public NonLongClickableUrlSpan(String url) { super(url); } @Override public void onClick(View widget) { if (widget.getTag() != null) { widget.setTag(null); return; } super.onClick(widget); } }
|
直接通过tag判断,这个TextView的Tag已经用做其他用途了?没事,setTag
还有一个重载版本:setTag(int, Object)
。
主要问题已经解决了,但可能还会遇到一点小问题,对于URLSpan,点击时后有个背景,有时松手后这个背景并不消失,解决方法也很简单,手动让它消失就行了,在onLongClick
中添加以下代码
1 2 3 4 5 6 7 8 9 10 11 12
| tv.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { ... TextView txt = (TextView) v; Spannable span = (Spannable) txt.getText(); Selection.removeSelection(span); return true; } });
|
前面说了简单针对TextView的情况,再来看一下其他情况,比如说ListView,假设它是一个聊天信息列表,每一项包含一个头像和一个消息内容,我们可能希望给这个消息内容加个长按事件,而这个长按事件的范围不包括头像,所以不能使用setOnItemLongClickListener
,只能给这个消息内容的布局设置OnLongClickListener
。这时可能会遇到一个问题:
如果消息内容中有一个TextView包含URLSpan,当长按时点击的位置在这个URLSpan上时,可能不会触发外层Layout的长按事件,这个TextView拦截了焦点。
这时可以给这个TextView也设置一个OnLongClickListener,URLSpan的长按处理和前面讲的一样,而长按事件的处理,可以简单地调用外层Layout的performLongClick()
方法。
如果这个外层布局设置了Selector Background,在长按包含URLSpan的TextView时外层Layout的背景是不会改变的,可以在这个Layout中添加android:addStatesFromChildren="true"
属性解决。