大家都熟悉微信朋友圈或者是贴吧里的某一条评论,
比如:小A回复小B:大吉大利,今晚吃鸡,哈哈哈。
点击小A和小B可以跳转到用户页面,点击整个Item就会响应其它事件,比如弹出键盘输入回复。
要实现这样的效果其实很简单,先自定义TextView,通过SpannableStringBuilder设置富文本格式,然后通过setText设置就可以了,看起来简单,但里面其实是有一些坑的,比如我实现了这种效果后,但发现点击Item其它地方的时候没有响应别的事件了。本篇文章就跟大家分享这个小知识点。先看效果:
创建Bean对象
创建评论对象和用户对象CommentBean和CommentUserSpan
public class CommentBean {/* 评论内容 */private String comment;/* 评论人 */private UserBean user;/* 回复人 */private UserBean replyUser;/* 省略了get和set方法 */......}public class UserBean {private String userId;private String userName;/* 省略了get和set方法 */......}
bean对象没什么可说的。
自定义TextView
public class CommentTextView extends TextView {public CommentTextView(Context context) {this(context, null);}public CommentTextView(Context context, @Nullable AttributeSet attrs) {this(context, attrs, 0);}public CommentTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);setMovementMethod(CommentUserMovementMethod.getInstance());}public void setText(CommentBean comment) {SpannableStringBuilder stringBuilder = new SpannableStringBuilder();if (comment.getUser() != null) {String str = comment.getUser().getUserName();stringBuilder.append(str);CommentUserSpan span = new CommentUserSpan(getContext(), comment.getUser());stringBuilder.setSpan(span, 0, str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);}if (comment.getReplyUser() != null) {stringBuilder.append("回复");int start = stringBuilder.toString().length();String str = comment.getReplyUser().getUserName();stringBuilder.append(str);CommentUserSpan span = new CommentUserSpan(getContext(), comment.getReplyUser());stringBuilder.setSpan(span, start, start + str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);stringBuilder.append(":");}stringBuilder.append(comment.getComment());setText(stringBuilder);}}
我们要想把CommentBean 中的两个用户对象转化成可点击的就需要借助ClickableSpan,通过SpannableStringBuilder 在特地位置setSpan就可以实现点击效果,ClickableSpan从名字上来看就知道是可点击的意思,上述代码中的CommentUserSpan就是自定义的继承自ClickableSpan用来实现点击事件的类。细心的朋友可能会发现构造里面有个setMovementMethod,没错,这个方法就是能够处理TextView中的触摸事件和点击事件的,有同学说不是有onTouchEvent方法吗?,对,但MovementMethod可以实现TextView中各种Span对象的处理,而且MovementMethod是onTouchEvent方法逻辑中的一部分,有兴趣的可以查看TextView的源码,我们来看看CommentUserSpan这个类:
public class CommentUserSpan extends ClickableSpan {private UserBean user;private Context context;private boolean isPressed;private int normalColor;private int pressedColor;public CommentUserSpan(Context context, UserBean commentUser) {super();this.user = commentUser;this.context = context;normalColor = Color.TRANSPARENT;pressedColor = context.getResources().getColor(R.color.colorPressed);}@Overridepublic void onClick(View widget) {Toast.makeText(context, "点击" + user.getUserName(), Toast.LENGTH_SHORT).show();}public void setPressed(boolean isPressed) {this.isPressed = isPressed;}@Overridepublic void updateDrawState(TextPaint ds) {ds.setColor(Color.BLUE);ds.bgColor = isPressed ? pressedColor : normalColor;}}
只需重写其中的onClick方法和updateDrawState方法,onClick用来处理点击事件,updateDrawState用来处理我们想要的视觉效果,比如文字颜色,背景颜色等等,ds.bgColor可以实现用户点击后背景变暗的效果,效果跟selector一样。这个类很简单,我们来看看CommentUserMovementMethod类:
public class CommentUserMovementMethod extends BaseMovementMethod {@Overridepublic boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {int action = event.getAction();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);CommentUserSpan[] link = buffer.getSpans(off, off, CommentUserSpan.class);CommentUserSpan span = null;if (link.length > 0) {span = link[0];}switch (action) {case MotionEvent.ACTION_DOWN:if (span != null) {span.setPressed(true);Selection.setSelection(buffer, buffer.getSpanStart(span), buffer.getSpanEnd(span));return true;} else {Selection.removeSelection(buffer);}break;case MotionEvent.ACTION_UP:if (span != null) {span.onClick(widget);span.setPressed(false);Selection.removeSelection(buffer);return true;}Selection.removeSelection(buffer);break;case MotionEvent.ACTION_MOVE:if (span != null) {span.setPressed(false);return true;} else {Selection.removeSelection(buffer);}break;default:if (span != null) {span.setPressed(false);}Selection.removeSelection(buffer);break;}return false;}public static MovementMethod getInstance() {if (sInstance == null) {sInstance = new CommentUserMovementMethod();}return sInstance;}private static CommentUserMovementMethod sInstance;}
这个类看起来就稍显复杂,其实也就做了两件事,
1、检测点击区域有没有我们设定的CommentUserSpan,有的话就处理相应的逻辑。
2、根据事件类型,处理CommentUserSpan是否是按压状态,用来实现点击效果的。
Android为TextView实现了三种MovementMethod,分别是ArrowKeyMovementMethod、LinkMovementMethod和ScrollingMovementMethod,从名字上来看就能很容易知道LinkMovementMethod是用来处理超链接的、ScrollingMovementMethod是用来处理滚动的,ArrowKeyMovementMethod一眼看不出来,其实大家也都熟悉,就是长安Edittext会出现选择文本,然后弹出ContextMenu,剪切、复制、粘贴这些操作。
CommentUserMovementMethod 直接继承了基类BaseMovementMethod 。主要是重写其中的onTouchEvent方法,最关键的一行是Layout layout = widget.getLayout(); 然后结合event.getX()和event.getY()就可以定位到点击位置的文字,然后查找这个位置有没有我们要处理的CommentUserSpan,有的话就消费这个点击事件,没有的话就不消费事件,抛给父View处理。
到这里基本上就已经实现了效果,但是,有一个非常不好的体验。就是当我点击非用户区域文字的时候发现响应不了整个Item的点击事件了,也就是我想点击整个条目然后弹出键盘这个效果实现不了了,难道点击其它区域的事件也被CommentUserMovementMethod 消费了?于是debug一下,发现也没有消费啊,一时间没有思路了,但可以肯定的是,事件一定被TextView消费了,找了半天,终于发现了坑,还记得CommentTextView 中调了setMovementMethod方法吗?这是源码:
public final void setMovementMethod(MovementMethod movement) {if (mMovement != movement) {mMovement = movement;if (movement != null && !(mText instanceof Spannable)) {setText(mText);}fixFocusableAndClickableSettings();// SelectionModifierCursorController depends on textCanBeSelected, which depends on// mMovementif (mEditor != null) mEditor.prepareCursorControllers();}}
其中关键的一句fixFocusableAndClickableSettings(); 处理TextView的焦点和点击。
private void fixFocusableAndClickableSettings() {if (mMovement != null || (mEditor != null && mEditor.mKeyListener != null)) {setFocusable(true);setClickable(true);setLongClickable(true);} else {setFocusable(false);setClickable(false);setLongClickable(false);}}
进去一看,恍然大悟,了解事件分发机制的同学应该都知道,当View的Clickable和LongClickable为true时,onTouchEvent方法必然会返回true,坑就在这。知道了原因,问题自然迎刃而解。我们可以在调用setMovementMethod方法后再把Clickable和LongClickable设为false就行。
public CommentTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);setMovementMethod(CommentUserMovementMethod.getInstance());setClickable(false);setLongClickable(false);}
好了,到这就完全实现微信朋友圈评论Item的效果了。
源码地址:
/469412882/CommentTextApp