在需要给TextView的某句话添加点击事件的时候,我们一般会使用ClickableSpan来进行富文本编辑。与此同时我们还需要配合
textView.setMovementMethod(LinkMovementMethod.getInstance());
方法才能使点击处理生效。但与此同时还会有一个问题:如果我们给父布局添加一个点击事件,需要在点击非链接的时候触发(例如RectclerView自定义的onItemClickListener有一部分就是给itemView添加onClick事件),但是设置了setMovementMethod方法后整个TextView就无法触发父布局的点击事件了,无论点击的地方是否有链接。
网络上大都是自定义TextView的onTouchEvent,把LinkMovementMethod方法的onTouch事件放到TextView中去处理,这么做侵入性太高。
其实TextView的setLinkMethod方法拦截所有点击事件的原因有两个。
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
if (links.length != 0) {
if (action == MotionEvent.ACTION_UP) {
links[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(links[0]),
buffer.getSpanEnd(links[0]));
}
return true;
} else {
Selection.removeSelection(buffer);
}
}
可以看到其实LinkMovementMethod方法本身判断逻辑是点击的位置是否有ClickSpan,如果有就返回true,如果没有就交给父布局,问题就在父布局这里,LinkMovementMethod 的父布局 ScrollingMovementMethod是控制内容滑动的MovementMethod,在它的onTouch方法中默认会给所有的点击事件返回true
case MotionEvent.ACTION_DOWN:
ds = buffer.getSpans(0, buffer.length(), DragState.class);
for (int i = 0; i < ds.length; i++) {
buffer.removeSpan(ds[i]);
}
buffer.setSpan(new DragState(event.getX(), event.getY(),
widget.getScrollX(), widget.getScrollY()),
0, 0, Spannable.SPAN_MARK_MARK);
return true;
case MotionEvent.ACTION_UP://简直前后呼应,down默认给加一个DragState,up就判断是否有DragState
ds = buffer.getSpans(0, buffer.length(), DragState.class);
for (int i = 0; i < ds.length; i++) {
buffer.removeSpan(ds[i]);
}
if (ds.length > 0 && ds[0].mUsed) {
return true;
} else {
return false;
}
因此,如果我们想要TextView只拦截处理ClickableSpan某些字段的事件,我们就需要重写LinkMovementMethod,把它的super.onTouchEvent直接改成false即可,这里建议做一个boolean设置,如果内容过长需要滑动,这个super还是需要加上的。但与此同时父布局的点击事件就无法再TextView上做响应了(某种意义上来说这两种情况也不可能同时发生)。
public final void setMovementMethod(MovementMethod movement) {
if (mMovement != movement) {
mMovement = movement;
if (movement != null && !(mText instanceof Spannable)) {
setText(mText);
}
fixFocusableAndClickableSettings();
if (mEditor != null) mEditor.prepareCursorControllers();
}
}
private void fixFocusableAndClickableSettings() {
if (mMovement != null || (mEditor != null && mEditor.mKeyListener != null)) {
setFocusable(FOCUSABLE);
setClickable(true);
setLongClickable(true);
} else {
setFocusable(FOCUSABLE_AUTO);
setClickable(false);
setLongClickable(false);
}
}
重点在fixFocusableAndClickableSettings方法上,它会给TextView把所有焦点事件都设置上。而TextView的onTouch事件通过源码可以看到。
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
//省略无关代码
final boolean superResult = super.onTouchEvent(event);
if (mEditor != null && mEditor.mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
//省略无关代码
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);
}
final boolean textIsSelectable = isTextSelectable();
//开了自动关联才会触发,省略
if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable)、、
//文字可以被修改才会触发,省略
if (touchIsFinished && (isTextEditable() || textIsSelectable))、、
if (handled) {
return true;
}
}
return superResult;
}
可以看到,如果movementMothed的onTouch方法返回false之后,实际上TextView返回的是super的onTouch,即View的onTouch,
而View.onTouch
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {//全是break,没有return ,所以省略.
case MotionEvent.ACTION_UP:
.
.
.
}
return true;
}
可以看到,只要clickable=true,onTouch都会拦截所有事件,关联上边的setLinkMovementMethod方法的源码,对clickAble,longClickAble的设置会导致onTouchEvent方法返回true。
解决方法归总:
1.重写LinkMovementMethod方法,根据需要控制super.ontouch()的返回值。
2.在setMovementMethod之前保存一下textView之前的xxxAble属性,设置完之后对这些属性进行还原。