在Android 里面,想要实现一段文字中部分文字可以点击就可以使用ClickableSpan,大概的方式
tv = (TextView) findViewById(R.id.tv_tsm_test);
SpannableStringBuilder builder = new SpannableStringBuilder("这个是连接");
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
tv.setBackgroundColor(Color.GREEN);
}
}, 3, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(builder);
tv.setMovementMethod(LinkMovementMethod.getInstance());
tv.setAutoLinkMask(Linkify.WEB_URLS);
实现的效果如下
点击连接这两个字就可以回调ClickableSpan 的 onClick方法将背景变为绿色,一个非常简单的应用, 现在产品又提出需求,要求我们给这个TextView 添加一个长按的事件, 心想这么简单的,几行代码就能实现,回去修改代码
tv = (TextView) findViewById(R.id.tv_tsm_test);
SpannableStringBuilder builder = new SpannableStringBuilder("这个是连接");
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
tv.setBackgroundColor(Color.GREEN);
}
}, 3, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(builder);
tv.setMovementMethod(LinkMovementMethod.getInstance());
tv.setAutoLinkMask(Linkify.WEB_URLS);
tv.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
tv.setBackgroundColor(Color.RED);
return true;
}
});
发现如果这个长按事件如果是在ClickableSpan上面响应的时候,同时也会回调ClickableSpan 的onClick事件,
这种情况是由LinkMovementMethod 导致的问题,查看他的源码,在onTouch中发现问题
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer,
MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
......
ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
if (links.length != 0) {
ClickableSpan link = links[0];
if (action == MotionEvent.ACTION_UP) {///只判断了抬起事件,没有判断时长
if (link instanceof TextLinkSpan) {
((TextLinkSpan) link).onClick(
widget, TextLinkSpan.INVOCATION_METHOD_TOUCH);
} else {
link.onClick(widget);
}
} else if (action == MotionEvent.ACTION_DOWN) {
........
}
return true;
} else {
Selection.removeSelection(buffer);
}
}
return super.onTouchEvent(widget, buffer, event);
}
发现在响应事件的地方判断只要是抬起事件,不管这个事件点击还是长按,他都会响应ClickableSpan 的onClick事件,我们需要修改一下这个方法,给定一个时长,当超过这个时长,就不响应点击事件
private static final long CLICK_DELAY = 1*1000;
if (link.length != 0) {
switch (action){
case MotionEvent.ACTION_UP:
long flag=(System.currentTimeMillis() - lastClickTime);
if (flag< CLICK_DELAY) {
link[0].onClick(widget);
}
return true;
case MotionEvent.ACTION_DOWN:
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
lastClickTime = System.currentTimeMillis();
return true;
}
} else {
Selection.removeSelection(buffer);
}
修改后的代码变成了这个样子,这次我们再来重新试一下,发现不会在长按的时候响ClickableSpan 的onClick事件了,你以为这样就结束了吗,并没有 产品又来了一个牛X的操作,他觉得长按复制要将所有文本都复制下来,她想要自由复制,这个长按复制体验并不好,她不想要了
在android 中想要实现TextView的复制功能其实也并不复杂,只需要将
android:textIsSelectable="true"
这个属性设置为true 就可以了,但是测试的时候发现 ,在点击ClickableSpan 的时候,会调用两次,
what ? 为什么会导致这个问题,我明明在LinkMovementMethod 的onTouch 里面才刚看到过,只有一个点击事件,为什么会响应两次,
经过了一番折腾后发现在TextView onTouchEvent 里面也有相应的判断, 代码案例如下
///抬起操作,并且有焦点
final boolean touchIsFinished = (action == MotionEvent.ACTION_UP)
&& (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
//enable 并且text 是Spannable
if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
&& mText instanceof Spannable && mLayout != null) {
boolean handled = false;
if (mMovement != null) {
handled |= mMovement.onTouchEvent(this, mSpannable, event);
}
final boolean textIsSelectable = isTextSelectable();
///意思是抬起操作并且有焦点 并且 链接可以被点击并且设置过setAutoLinkMask 这个属性 同时 textIsSelectable=true
///在所有的条件都满足的情况下,就会调用ClickableSpan 的onClick事件,
if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
// The LinkMovementMethod which should handle taps on links has not been installed
// on non editable text that support text selection.
// We reproduce its behavior here to open links for these.
ClickableSpan[] links = mSpannable.getSpans(getSelectionStart(),
getSelectionEnd(), ClickableSpan.class);
if (links.length > 0) {
links[0].onClick(this);
handled = true;
}
}
实际项目中,由于代码是祖传的,修改的时候不能修改比较重要的属性,所以不能将原来的setAutoLinkMask(Linkify.WEB_URLS);这个属性去掉,所以只能修改这个mLinksClickable ,让他不满足情况,就不会影响我们的事件了,在实际开发过程中也可以将setMovementMethod(LinkMovementMethod.getInstance()); 这段代码移除,或者尝试移除setAutoLinkMask(Linkify.WEB_URLS); 这段代码,我选择的是
android:linksClickable="false"
在布局中添加这个属性,就可以达到我们想要的效果了,点击事件只会响应一次了,
但是在实际开发过程中,我修改的代码是在组件里面,使用这个组件的的应用有好几个,那么就要根据功能开关动态的在代码中去修改这些属性
实际项目中代码为
SpannableStringBuilder builder = SpannableUtil.addInnerLink(vh.tv, msg, color, needLinkUnderLine, new
SpannableUtil.LinkCallback() {
@Override
public void onLinkClick(String originText, String link, int startIndex, int endIndex) {
}
});
vh.tv.setText(SpannableUtil.addPhoneLink(msg.getMsgContent(), builder, mExtAdapter.isAddPhoneLink(), color, new
SpannableUtil.LinkCallback() {
@Override
public void onLinkClick(String originText, String link, int startIndex, int endIndex) {
}
}), TextView.BufferType.SPANNABLE);
if(打开自由复制){
vh.tv.setLinksClickable(false);
vh.tv.setTextIsSelectable(true);
}
这段代码看起来没有什么问题,但是在部分机型中发现有概率会让ClickableSpan 的onClick事件的回调不调用,真是让这个ClickableSpan 给我狠狠的教育了一顿,问题一个接着一个,没办法只能继续去分析,
查看TextView setTextIsSelectable 方法
public void setTextIsSelectable(boolean selectable) {
if (!selectable && mEditor == null) return; // false is default value with no edit data
createEditorIfNeeded();
if (mEditor.mTextIsSelectable == selectable) return;
mEditor.mTextIsSelectable = selectable;
setFocusableInTouchMode(selectable);
setFocusable(FOCUSABLE_AUTO);
setClickable(selectable);
setLongClickable(selectable);
// mInputType should already be EditorInfo.TYPE_NULL and mInput should be null
setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
setText(mText, selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
// Called by setText above, but safer in case of future code changes
mEditor.prepareCursorControllers();
}
重点是 setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null); 这个地方,如果设置的是可以自由复制,那么就使用ArrowKeyMovementMethod ,否则将MovementMethod 设置为null ,将我们的LinkMovementMethod给替换掉了,所以将 vh.tv.setTextIsSelectable(true);这个方法提前到祖传代码setMovementMethod 之前,问题得到解决