仿微信评论点击事件

583 阅读5分钟

「这是我参与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的区别,到此,就可以实现需要实现的效果了!

demo地址:download.csdn.net/download/ba…