客户端注册账号,需要用户确认用户协议及可以查看协议。
选择使用单CheckBox控件配合SpannableString。
但是遇到了一个问题,点击链接后CheckBox也响应了Click导致CheckBox勾选状态改变,这是不期望发生的。
对这个问题,原因查找及解决方式如下。

显示效果

初始代码

1
2
3
4
5
6
7
8
9
10
11
12
CheckBox test = (CheckBox) v.findViewById(R.id.test);
test.setOnClickListener(this);
SpannableString s = new SpannableString("这是一个链接");
s.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.360.cn"));
startActivity(intent);
}
}, 4, 6, Spanned.SPAN_MARK_MARK);
test.setText(s);
test.setMovementMethod(LinkMovementMethod.getInstance());

问题现象

点击链接后不只浏览器被激活并打开www.360.cn,同时复选框勾选状态改变,OnClickListener被激活,如果有其他CheckBox监听也会同时被激活。

问题原因

点击事件,从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
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();

if (mEditor != null) mEditor.onTouchEvent(event);

final boolean superResult = super.onTouchEvent(event);

/*
* Don't handle the release after a long press, because it will
* move the selection away from whatever the menu action was
* trying to affect.
*/
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);
}

其中开始的

1
final boolean superResult = super.onTouchEvent(event);

调用了View中得onTouchEvent方法,其中

1
2
3
4
5
6
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}

激活了控件的Click效果。
从代码分析,这个Click效果可能立即激活,也可能作为一个Message被推入队列稍后执行。
由于现在的主要任务不在这里,就不更深入时序了。

继续回到TextView的onTouchEvent方法中,后面的代码

1
handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);

这里调用了我们代码LinkMovementMethod的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
30
31
32
33
34
35
36
37
38
39
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer,
MotionEvent event) {
int action = event.getAction();

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[] link = buffer.getSpans(off, off, ClickableSpan.class);

if (link.length != 0) {
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]));
}

return true;
} else {
Selection.removeSelection(buffer);
}
}

return super.onTouchEvent(widget, buffer, event);
}

其中

1
link[0].onClick(widget);

调用了我们代码中ClickableSpan的onClick方法

解决问题

在TextView的onTouchEvent执行前,拦截ACTION_UP方法,并修改为ACTION_CANCEL,避免触发控件performClick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
test.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (v instanceof TextView) {
TextView text = (TextView) v;
MovementMethod method = text.getMovementMethod();
if (method != null && text.getText() instanceof Spannable
&& event.getAction() == MotionEvent.ACTION_UP) {
if (method.onTouchEvent(text, (Spannable) text.getText(), event)) {
event.setAction(MotionEvent.ACTION_CANCEL);
}
}
}
return false;
}
});

完美解决