「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」
本文是针对含有类似于QQ空间、微信朋友圈模块的App,在评论布局以及评论者的点击事件部分,仿照qq微信的处理逻辑来写的一篇思路总结文章,不知道具体效果的可以打开微信朋友圈,在有评论的地方点击几下试试效果,并也同时思考下如果让你做,该怎么实现?! 先来看下具体要实现的效果:
效果一: 1.用户名和评论内容字体颜色区分; 2.用户名的点击事件; 3.整条评论的点击事件; 4.用户名的点击背景效果; 5.整条评论的点击背景效果;
效果二: 无论所点击的评论在屏幕中的任何位置,点击后都会自动紧挨评论输入框之上;
先说效果一,可能大家会觉得,用SpannableString,分分钟搞定,是的,当然是要用SpannableString,一般思路就是: 通过SpannableString.setSpan,设置整条评论内容中评论者昵称的颜色(ForegroundColorSpan)以及点击事件(ClickableSpan),接着textview.setText,最后再设置textview的点击事件textview.setOnClickListener;
其实大致就是这个思路,只不过真正实现起来就会遇到不少问题: 1.某段字符的点击事件也就是使用了ClickableSpan后,会跟textview的点击事件冲突; 2.字符的点击背景和整个textview的点击背景效果如何设置以及如何解决两者点击时背景效果的冲突;
如果仅仅是通过以下代码:
SpannableString spannableString=new SpannableString("xxx");
spannableString.setSpan(new ClickableSpan() {
@Override
public void onClick(View view) {
}
},0,1,Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);
textView.setMovementMethod(LinkMovementMethod.getInstance());//启用ClickableSpan点击事件
textView.setOnClickListener{...}
那么ClickableSpan的点击事件就会跟textView的点击事件冲突,点击时只会响应textView的点击事件,那么此时就需要重写TextView中的performClick(模拟点击),onTouchEvent,以及LinkMovementMethod中的onTouchEvent方法;上面所说的冲突,其实就是在点击(触摸touch)用户名时,最终的事件流程走到了textview的performClick并执行了onClick:
点击用户名: textview-onTouchEvent>>LinkMovementMethod-onTouchEvent>>textview-performClick>>textview-onClick
我们需要做的就是阻断textview-performClick>>textview-onClick的事件走向,具体做法就是在LinkMovementMethod-onTouchEvent中判断是否点击了用户名(也就是设置了ClickableSpan的那段字符),然后在textview的performClick方法中直接返回true即可阻断!
当解决完点击事件冲突后,可以给整个textview设置背景,但怎么给其中的某段字符设置背景呢,答案肯定还是通过setSpan(BackgroundColorSpan),但这样一来这段字符就被设置成了固定背景,我们需要的是点击时有背景,手指抬起时就不需要有背景了,所以需要在LinkMovementMethod-onTouchEvent中的ACTION_DOWN和ACTION_UP中动态设置就可以了!
另外如果设置了ClickSpan就不需要再单独设置字符颜色了,可以直接在ClickSpan的updateDrawState方法中设置:
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(color);
ds.setUnderlineText(false);
}
够罗嗦,上代码,整个重写的TextView以及LinkMovementMethod代码如下:
@SuppressLint("AppCompatCustomView")
public class ClickTextView extends TextView {
Context context;
public ClickTextView(Context context) {
super(context);
this.context = context;
setTextSize(14);
setPadding(0, 5, 0, 5);
setBackgroundResource(R.drawable.view_selector);
setTextColor(Color.parseColor("#2B3A50"));
}
public ClickTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public ClickTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setReplyText(WxCircleModel.ReplyModel replyModel) {
SpannableString spannableString;
if (TextUtils.isEmpty(replyModel.getReply_to_username())) {
spannableString = new SpannableString(replyModel.getReply_username() + ":" + replyModel.getReply_content());
spannableString.setSpan(new CustomClickableSpan(context, replyModel, 1), 0, replyModel.getReply_username().length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
} else {
spannableString = new SpannableString(replyModel.getReply_to_username() + " 回复 " + replyModel.getReply_username() + ":" + replyModel.getReply_content());
spannableString.setSpan(new CustomClickableSpan(context, replyModel, 2), 0, replyModel.getReply_to_username().length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
spannableString.setSpan(new CustomClickableSpan(context, replyModel, 1), replyModel.getReply_to_username().length() + 4, replyModel.getReply_to_username().length() + 4 + replyModel.getReply_username().length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
setText(spannableString);
setMovementMethod(CustomLinkMovementMethod.getInstance());
setHighlightColor(Color.parseColor("#00000000"));
}
/**
* 自定义ClickableSpan
*/
public static class CustomClickableSpan extends ClickableSpan {
Context context;
int color;
WxCircleModel.ReplyModel replyModel;
int clicktype;
public CustomClickableSpan(Context context, WxCircleModel.ReplyModel replyModel, int clicktype) {
this.context = context;
this.replyModel = replyModel;
this.clicktype = clicktype;
color = Color.parseColor("#5B6FCA");
}
@Override
public void onClick(View widget) {
switch (clicktype) {
case 1://reply
ToastUtil.showToast(context, replyModel.getReply_username());
break;
case 2://reply to
ToastUtil.showToast(context, replyModel.getReply_to_username());
break;
}
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(color);
ds.setUnderlineText(false);
}
}
public boolean isClick;//内部链接是否被点击
@Override
public boolean performClick() {
if (isClick) {
return true;
}
return super.performClick();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
isClick = false;
setBackgroundResource(R.drawable.view_selector);
return super.onTouchEvent(event);
}
/**
* 自定义LinkMovementMethod
*/
public static class CustomLinkMovementMethod extends LinkMovementMethod {
static CustomLinkMovementMethod sInstance;
@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);
buffer.setSpan(new BackgroundColorSpan(Color.parseColor("#00000000")), buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if (action == MotionEvent.ACTION_DOWN) {
buffer.setSpan(new BackgroundColorSpan(Color.parseColor("#E0E0E0")), buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
if (widget instanceof ClickTextView) {
((ClickTextView) widget).isClick = true;
widget.setBackground(null);
}
return true;
} else {
Selection.removeSelection(buffer);
super.onTouchEvent(widget, buffer, event);
return false;
}
}
return Touch.onTouchEvent(widget, buffer, event);
}
public static CustomLinkMovementMethod getInstance() {
if (sInstance == null) {
sInstance = new CustomLinkMovementMethod();
}
return sInstance;
}
}
}
接下来说效果二该如何实现?我首先想到的就是如何获取评论textview在整个屏幕中的y坐标(textview离屏幕顶部的高度),然后再获取键盘高度,屏幕高度-键盘高度=键盘顶部离屏幕顶部的高度,最后在点击评论时,RecyclerView只需scroll键盘顶部离屏幕顶部的高度-textview离屏幕顶部的高度即可!
ok,首先来获取textview的坐标,其实很好做,通过textview的onTouch来获取:
textView.setOnTouchListener((view, motionEvent) -> {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN)
Y = motionEvent.getRawY() + (view.getHeight() - motionEvent.getY());
return false;
});
getRawY是触点在整个屏幕中的y坐标,而getY是触点在该view中的y坐标,那么为什么不直接取Y = motionEvent.getRawY(),而是来了Y = motionEvent.getRawY() + (view.getHeight() - motionEvent.getY())这么个计算呢?
并不难理解,view.getHeight() 是整个textview高度,motionEvent.getY())是触点离textview顶部的高度,motionEvent.getRawY()是触点离屏幕顶部的高度,那么最终得到的结果就是textview最底部离屏幕顶部的高度了,对吧?!
接下来需要获取软键盘的高度,但sdk里面并没有直接提供获取键盘高度的api,那么我们就需要通过view的高度高度变化来动态获取键盘高度:
mRootView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
float keyBoardH = mRootView.getRootView().getHeight() - SysUtils.getStatusBarHeight(this) - mRootView.getHeight();//键盘高度
if (keyBoardH > 300) {//键盘弹起
float viewVisibleH = mRootView.getHeight() + SysUtils.getStatusBarHeight(this)-SysUtils.Dp2Px(this,50);
mRecyclerView.scrollBy(0, (int) (commentTextY - viewVisibleH));
}
});
注意:mRootView是除了状态栏后的整个view,记住千万不要带上ActionBar,主题要设置为NoActionBar才能精确计算!
mRootView.getRootView().getHeight()是整个屏幕高度,mRootView.getHeight()是mRootView可见部分的高度,SysUtils.getStatusBarHeight(this)是获取状态栏高度,而commentTextY就是上面获取到的评论textview最底部离屏幕顶部的高度,再结合上上图,应该可以理解吧!因为我们还有一个输入框的高度50dp(这里尽量设置为固定高度方便计算),
所以计算时也需要减掉SysUtils.Dp2Px(this,50)像素,另外 ,大家还需要了解RecyclerView的scrollTo和scrollBy的区别,到此,就可以实现需要实现的效果了!