上一篇文章解决了UrlSpan与文本中url链接识别和设置的问题,超链接可以正常显示了。但由于用户期望TextView中的文章内容可以自由复制,这个需求嘛实现起来就是设置下textView.setTextIsSelectable(true)就可以了。
天真的以为没啥问题,版本转测。立马出现一个严重问题,第一次点击超链接打开的网址与实际不符,第二次点击超链接才能打开正确的网址,或者说每次点击超链接文本打开的网址都是上一次点击区域内的超链接url。
贴TextView::onTouchEvent的源码
02 |
public boolean onTouchEvent(MotionEvent
event) { |
04 |
final boolean superResult
= super .onTouchEvent(event); |
05 |
if ((mMovement
!= null ||
onCheckIsTextEditor()) && isEnabled() |
06 |
&&
mText instanceof Spannable
&& mLayout != null )
{ |
07 |
boolean handled
= false ; |
09 |
if (mMovement
!= null )
{ |
10 |
handled
|= mMovement.onTouchEvent( this ,
(Spannable) mText, event); |
13 |
if (touchIsFinished
&& mLinksClickable && mAutoLinkMask != 0 &&
mTextIsSelectable) { |
17 |
ClickableSpan[]
links = ((Spannable) mText).getSpans(getSelectionStart(), |
18 |
getSelectionEnd(),
ClickableSpan. class ); |
20 |
if (links.length
!= 0 )
{ |
21 |
links[ 0 ].onClick( this ); |
可以看到onTouchEvent()方法中涉及ClickableSpan处理的有两个关键点
关键点1. 执行MovementMothed::onTouchEvent()方法(上面源码中蓝色部分)------mMovement.onTouchEvent(this,
(Spannable) mText, event);
关键点2.上面源码中蓝色部分,在TextView的LinksClickable、AutoLink、TextIsSelectable属性都已设置情况下,获取当前选择区域内的ClickableSpan并执行其onClick()方法;
对于1,支持ClickableSpan使用的MovementMothed为LinkMovementMethod。而支持文本自由复制的MovementMothed为ArrowKeyMovementMethod。
设置了TextView的textIsSelectable属性后,TextView会自动设置器mMovement成员为ArrowKeyMovementMethod实例。此时文本才可以自由复制。也即上面关键点1中执行的是ArrowKeyMovementMethod::onTouchEvent()方法:长按时弹出复制、粘贴、选择菜单。
而我也正好设置了TextView的autoLink属性,故关键点2的if判断通过,但此时TextView中当前选择文本为记录的用户上次点击位置,故打开的链接也就是用户上次点击位置对应的链接url了,若用户上次点击位置没有超链接,就不会弹出浏览器打开链接的页面。
当然若去掉TextView的autoLink属性,用户点击超链接将不会产生任何操作。
总结:TextView的超链接点击打开和文本自由复制两个功能是互斥的,没办法同时支持。终于只要为啥百度贴吧
帖子中不能自由复制文本了。。。
两个功能都很重要,不可能删除哪一个。相比较之下文本自由复制功能是帖子内容TextView中所有文本都需要支持的功能,而超链接点击打开功能仅仅是超链接部分文本需要的。故我选择设置TextView的textIsSelectable属性,即默认支持文本自由复制(此时不能修改mMovement为ArrowKeyMovementMethod外的其他MovementMothed,不然自由复制功能将会失效)。然后重写TextView::onTouchEvent()方法来支持超链接点击打开功能。具体如下:
01 |
public class CustomTextView extends TextView
{ |
03 |
private long mLastActionDownTime
= - 1 ; |
04 |
public CustomTextView(Context
context) { |
08 |
public CustomTextView(Context
context, AttributeSet attrs) { |
09 |
super (context,
attrs); |
12 |
public CustomTextView(Context
context, AttributeSet attrs, int defStyle)
{ |
13 |
super (context,
attrs, defStyle); |
17 |
public boolean onTouchEvent(MotionEvent
event) { |
18 |
CharSequence
text = getText(); |
19 |
if (text
!= null &&
text instanceof Spannable)
{ |
20 |
handleLinkMovementMethod( this ,
(Spannable)text, event); |
23 |
return super .onTouchEvent(event); |
26 |
private boolean handleLinkMovementMethod(TextView
widget, Spannable buffer, MotionEvent event) { |
27 |
int action
= event.getAction(); |
29 |
if (action
== MotionEvent.ACTION_UP || |
30 |
action
== MotionEvent.ACTION_DOWN) { |
31 |
int x
= ( int )
event.getX(); |
32 |
int y
= ( int )
event.getY(); |
34 |
x
-= widget.getTotalPaddingLeft(); |
35 |
y
-= widget.getTotalPaddingTop(); |
37 |
x
+= widget.getScrollX(); |
38 |
y
+= widget.getScrollY(); |
40 |
Layout
layout = widget.getLayout(); |
41 |
int line
= layout.getLineForVertical(y); |
42 |
int off
= layout.getOffsetForHorizontal(line, x); |
44 |
ClickableSpan[]
link = buffer.getSpans(off, off, ClickableSpan. class ); |
46 |
if (link.length
!= 0 )
{ |
47 |
if (action
== MotionEvent.ACTION_UP) { |
48 |
long actionUpTime
= System.currentTimeMillis(); |
49 |
if (actionUpTime
- mLastActionDownTime > ViewConfiguration.getLongPressTimeout()) { //长按事件,取消LinkMovementMethod处理,即不处理ClickableSpan点击事件 |
52 |
link[ 0 ].onClick(widget); |
53 |
Selection.removeSelection(buffer); |
54 |
} else if (action
== MotionEvent.ACTION_DOWN) { |
55 |
Selection.setSelection(buffer,
buffer.getSpanStart(link[ 0 ]),
buffer.getSpanEnd(link[ 0 ])); |
56 |
mLastActionDownTime
= System.currentTimeMillis(); |
上面的TextView::handleLinkMovementMethod()方法,其实就是从LinkMovementMethod::onTouchEvent()方法,稍微修改了下。主要就是两个逻辑:
1.ACTION_DOWN时,将点击位置的超链接选中;
2.ACTION_UP时,在非长按情况下,执行ClickableSpan的点击处理。
然后就完美解决了TextView的超链接点击打开和文本自由复制两个功能是互斥问题。