利用SpannableString可以是TextView变的很花哨,比如某些字红,某些字绿,某些字有下划线
部分着色
来看个简单的例子
String name = "郭敬明";
String message = ":我是小白鼠";
SpannableString spanStr = new SpannableString(name + message);
spanStr.setSpan(new ForegroundColorSpan(Color.RED),0,3,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mClickableText.setText(spanStr);
有点击事件
还可以让部分文字有点击事件
String name = "郭敬明";
String message = ":我是小白鼠";
SpannableString spanStr = new SpannableString(name + message);
spanStr.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
Toast.makeText(MainActivity.this,"点击了郭敬明",Toast.LENGTH_SHORT).show();
}
},0,3,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spanStr.setSpan(new ForegroundColorSpan(Color.RED),0,3,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mClickableText.setText(spanStr);
mClickableText.setMovementMethod(LinkMovementMethod.getInstance());
着色的setSpan和设置点击事件的setSpan之间的顺序,应该是后写着色,否则颜色不对
有了部分点击事件之后,会自动加一个下划线,如果我们不想要这个下划线怎么办?
去掉下划线
方法:重写ClickableSpan
public class CustomSpan extends ClickableSpan {
private Context context;
public CustomSpan(Context context) {
this.context = context;
}
@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(Color.RED);
ds.setUnderlineText(false);
ds.setTextSize(ScreenUtil.dip2px(16));
// ds.setFakeBoldText(false);
}
@Override
public void onClick(View widget) {
Toast.makeText(context, "点击了郭敬明", Toast.LENGTH_SHORT).show();
}
}
在updateDrawState内设置颜色和取消下划线,这样只需要setSpan一次就可以了,Main代码如下
String name = "郭敬明";
String message = ":我是小白鼠";
SpannableString spanStr = new SpannableString(name + message);
spanStr.setSpan(new CustomSpan(this),0,3,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mClickableText.setText(spanStr);
mClickableText.setMovementMethod(LinkMovementMethod.getInstance());
点击触发2次事件问题
如果对上面的TextView设置了点击事件,会发现,点击“郭敬明”,不仅仅会触发他自己的点击事件,也会触发TextView的点击事件,比较不合理,事实上的却如此,看TextView源码可以发现,ClickableSpan的onClick触发是,
handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); (在TextView的onTouchEvent内)
mMovement.onTouchEvent并不会消费点击事件,而且在这之前就走了 super.onTouchEvent(event),这里的super是View,这里面就会调post(mPerformClick),这就是准备处理点击事件(可以参考文章
http://blog.youkuaiyun.com/litefish/article/details/44172659)。可以看到这里是先走的super.onTouchEvent(event),然后走mMovement.onTouchEvent,可是实际效果相反,实际显示的onClick调用顺序是先 部分文字的,然后TextView的,这是怎么回事呢?看代码流程。首先post(mPerformClick),把TextView的点击事件丢到消息队列里,然后走mMovement.onTouchEvent,这里会直接调用 部分文字的 onClick事件 ,然后再消息泵内会处理TextView的点击事件。真想大白!
部分源码如下
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);
}
触发2次onClick解决方案
如何解决呢?
解决方案1
/**
* 改写{@link android.text.method.LinkMovementMethod},主要改写了onTouchEvent,避免了 “某些文字”的点击即会触发他们自己的OnClick也会触发TextView的OnClick,用CustomLinkMovementMethod之后就不写TextView的OnClick,
* 而是写TextClickedListener
*/
public class CustomLinkMovementMethod extends ScrollingMovementMethod {
private static final int CLICK = 1;
private static final int UP = 2;
private static final int DOWN = 3;
private static CustomLinkMovementMethod sInstance;
public static CustomLinkMovementMethod getInstance() {
if (sInstance == null)
sInstance = new CustomLinkMovementMethod();
return sInstance;
}
public abstract interface TextClickedListener {
public abstract void onTextClicked();
}
TextClickedListener listener = null;
public void setOnTextClickListener(TextClickedListener listen) {
listener = listen;
}
@Override
public boolean onKeyDown(TextView widget, Spannable buffer,
int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
if (event.getRepeatCount() == 0) {
if (action(CLICK, widget, buffer)) {
return true;
}
}
}
return super.onKeyDown(widget, buffer, keyCode, event);
}
@Override
protected boolean up(TextView widget, Spannable buffer) {
if (action(UP, widget, buffer)) {
return true;
}
return super.up(widget, buffer);
}
@Override
protected boolean down(TextView widget, Spannable buffer) {
if (action(DOWN, widget, buffer)) {
return true;
}
return super.down(widget, buffer);
}
@Override
protected boolean left(TextView widget, Spannable buffer) {
if (action(UP, widget, buffer)) {
return true;
}
return super.left(widget, buffer);
}
@Override
protected boolean right(TextView widget, Spannable buffer) {
if (action(DOWN, widget, buffer)) {
return true;
}
return super.right(widget, buffer);
}
private boolean action(int what, TextView widget, Spannable buffer) {
boolean handled = false;
Layout layout = widget.getLayout();
int padding = widget.getTotalPaddingTop() +
widget.getTotalPaddingBottom();
int areatop = widget.getScrollY();
int areabot = areatop + widget.getHeight() - padding;
int linetop = layout.getLineForVertical(areatop);
int linebot = layout.getLineForVertical(areabot);
int first = layout.getLineStart(linetop);
int last = layout.getLineEnd(linebot);
ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
int a = Selection.getSelectionStart(buffer);
int b = Selection.getSelectionEnd(buffer);
int selStart = Math.min(a, b);
int selEnd = Math.max(a, b);
if (selStart < 0) {
if (buffer.getSpanStart(FROM_BELOW) >= 0) {
selStart = selEnd = buffer.length();
}
}
if (selStart > last)
selStart = selEnd = Integer.MAX_VALUE;
if (selEnd < first)
selStart = selEnd = -1;
switch (what) {
case CLICK:
if (selStart == selEnd) {
return false;
}
ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
if (link.length != 1)
return false;
link[0].onClick(widget);
break;
case UP:
int beststart, bestend;
beststart = -1;
bestend = -1;
for (int i = 0; i < candidates.length; i++) {
int end = buffer.getSpanEnd(candidates[i]);
if (end < selEnd || selStart == selEnd) {
if (end > bestend) {
beststart = buffer.getSpanStart(candidates[i]);
bestend = end;
}
}
}
if (beststart >= 0) {
Selection.setSelection(buffer, bestend, beststart);
return true;
}
break;
case DOWN:
beststart = Integer.MAX_VALUE;
bestend = Integer.MAX_VALUE;
for (int i = 0; i < candidates.length; i++) {
int start = buffer.getSpanStart(candidates[i]);
if (start > selStart || selStart == selEnd) {
if (start < beststart) {
beststart = start;
bestend = buffer.getSpanEnd(candidates[i]);
}
}
}
if (bestend < Integer.MAX_VALUE) {
Selection.setSelection(buffer, beststart, bestend);
return true;
}
break;
}
return false;
}
public boolean onKeyUp(TextView widget, Spannable buffer,
int keyCode, KeyEvent event) {
return false;
}
@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);
if (action == MotionEvent.ACTION_UP) {
if (listener != null)
listener.onTextClicked();
}
}
}
return super.onTouchEvent(widget, buffer, event);
}
public void initialize(TextView widget, Spannable text) {
Selection.removeSelection(text);
text.removeSpan(FROM_BELOW);
}
public void onTakeFocus(TextView view, Spannable text, int dir) {
Selection.removeSelection(text);
if ((dir & View.FOCUS_BACKWARD) != 0) {
text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
} else {
text.removeSpan(FROM_BELOW);
}
}
private static Object FROM_BELOW = new NoCopySpan.Concrete();
}
可以使用CustomLinkMovementMethod来代替默认的LinkMovementMethod,代码基本一致,主要区别是onTouchEvent的时候,加了点代码
if (action == MotionEvent.ACTION_UP) {
if (listener != null)
listener.onTextClicked();
}
这是什么意思呢?
如果点击了“郭敬明”区域,会走到 link[0].onClick(widget);,那就调用了ClickableSpan的onClick。
如果点击了TextView的其他部分,就会走到listener.onTextClicked();
所以我们可以去掉原来的TextView的onClick,用这里的onTextClicked代替,用法如下
CustomLinkMovementMethod cc=new CustomLinkMovementMethod();
cc.setOnTextClickListener(new CustomLinkMovementMethod.TextClickedListener() {
@Override
public void onTextClicked() {
}
});
mClickableText.setMovementMethod(cc);
解决方案2
直接设置2个Span
spanStr.setSpan(new CustomSpan(this),0,3,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spanStr.setSpan(new CustomSpan1(this),0,end,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
CustomSpan里写,“郭敬明”的点击回调
CustomSpan1里写整个textView的点击回调
这里有个问题,我们可以看到CustomSpan是针对前3个字符,CustomSpan1是针对全部字符,那也就是说
“郭敬明” 对应了2个Span,那结果会怎么样呢?看源码(LinkMovementMethod.onTouchEvent)
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);
}
可以看到首先会去查当前字符有几个span,放入link里,然后只处理link[0]的onClick,所以这里,“郭敬明”也只会走一个onClick.
注意这里span是根据字符查找的,不是根据鼠标位置,他首先根据鼠标位置找到当前字符(如果当前位置没有字符,比如点在padding处,那他会找最近的字符),然后根据字符来查找span,再根据span来处理