TextView实现点击部分文字跳转,实现微信朋友圈评论Item的显示效果

xiaoxiao2021-02-28  5

大家都熟悉微信朋友圈或者是贴吧里的某一条评论,

比如: 小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); } @Override public void onClick(View widget) { Toast.makeText(context, "点击" + user.getUserName(), Toast.LENGTH_SHORT).show(); } public void setPressed(boolean isPressed) { this.isPressed = isPressed; } @Override public 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 { @Override public 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 // mMovement if (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的效果了。

源码地址:

https://github.com/469412882/CommentTextApp

转载请注明原文地址: https://www.6miu.com/read-1600360.html

最新回复(0)