博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
仿微信评论控件封装
阅读量:6271 次
发布时间:2019-06-22

本文共 8761 字,大约阅读时间需要 29 分钟。

###1. 需求前提说明

越来越多的应用为了增强用户粘性,选择在应用内部整合类似微信朋友圈的模块。这样的模块提供了各式各样的图文混排效果,在wifi情况下能对视频进行自动预览,有交互良好(开发复杂)的评论体验。开发这一模块,对每个细节的性能要求更为严格,为保证列表滑动的流畅性。由于公司战略调整,添加动态功能(朋友圈),楼主主要负责这一模块的开发,如果有相关问题可以留言讨论,本篇幅主要讲评论组件的封装。

###2. 镇楼图

功能交互点:

  • 点击用户昵称,仅昵称文字区域展示按下背景图,跳转进入用户个人主页。
  • 点击用户评论,评论+昵称区域展示按下背景图,进行回复评论。
  • 长按用户昵称,同(1)效果。
  • 长按用户评论,评论+昵称区域展示按下背景图,用于删除评论。

###3. 思路分析

  • Step One: 采用TextView + ClickableSpan

  1. 代码实现
commentText.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, "回复评论", Toast.LENGTH_SHORT).show(); } }); commentText.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { Toast.makeText(MainActivity.this, "删除评论", Toast.LENGTH_SHORT).show(); return true; } }); commentText.setMovementMethod(LinkMovementMethod.getInstance()); CharSequence text = commentText.getText(); SpannableString spannableString = new SpannableString(text); spannableString.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { Toast.makeText(MainActivity.this, "进入个人主页", Toast.LENGTH_SHORT).show(); } }, 0, 3, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE); commentText.setText(spannableString);复制代码
  1. 完成度: 完成控件评论区域的点击和长按,即功能交互点的第2点和第4点。
  2. 缺陷:点击昵称区域会触发TextView的OnClickListener事件,长按昵称区域,会触发TextView的OnLongClickListener事件。
  • Step Two: ClickableSpan对事件(Click、LongClick)进行消费

  1. 先查看源码,TextView如何调用ClickableSpan中的OnClick方法:
public class TextView extends View {    @Override    public boolean onTouchEvent(MotionEvent event) {        ...        final boolean touchIsFinished = (action == MotionEvent.ACTION_UP)                && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();             // 1. TextView 如果没有调用setMovementMethod(xx) 设置ClickableSpan不会生效        if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()                && mText instanceof Spannable && mLayout != null) {            boolean handled = false;            if (mMovement != null) {            // 2. 由setMovementMethod()设置的MovementMethod处理此次点击。                handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);            }                       ...   // 3. 交由TextView处理            if (handled) {                return true;            }        }        return superResult;    }}复制代码
  1. 原因: 从代码可以看出,TextView在OnTouchEvent方法中对ClickableSpan中的OnClick进行回调处理,但并没有消费掉此次事件直接返回,而是继续交予TextView处理(可能触发TextView的OnClick和OnLongClick)。
  1. 解决: 重写TextView的OnTouchEvent方法,先判断点击的区域是否是ClickableSpan,如果是,交由ClickableSpan处理后直接return true返回。
  1. 疑问: 那么如何判断点击的区域为ClickableSpan? 之前在分析TextView的OnTouchEvent方法中第2点注释,可以知道mMovement.onTouchEvent必定隐藏了判断逻辑。
public class LinkMovementMethod extends ScrollingMovementMethod { @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();  //获取点击区域在TextView的横向位置            int y = (int) event.getY();            x -= widget.getTotalPaddingLeft();   // 减去TextView左边的padding值,获取TextView文字的【可见】起始位置偏移            y -= widget.getTotalPaddingTop();            x += widget.getScrollX();  // 可见文字起始偏移 + 左边因为滑动被隐藏的文字宽度 = 当前点击文字的排布位置            y += widget.getScrollY();            Layout layout = widget.getLayout();  // 获取TextView上文字的排版            int line = layout.getLineForVertical(y);  // 根据Y坐标获取点击位置的行数            int off = layout.getOffsetForHorizontal(line, x);  // 根据行数和水平X量获取当前点击位置距离第一个文字左边的偏移量            ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);  // 根据偏移量获取ClickSpan            if (links.length != 0) {                if (action == MotionEvent.ACTION_UP) {                    links[0].onClick(widget);                } else if (action == MotionEvent.ACTION_DOWN) {                    Selection.setSelection(buffer,                        buffer.getSpanStart(links[0]),                        buffer.getSpanEnd(links[0]));                }                return true;            } else {                Selection.removeSelection(buffer);            }        }        return super.onTouchEvent(widget, buffer, event);    }}复制代码

###4. 控件封装

@SuppressLint("AppCompatCustomView")public class CommentTextView extends TextView {    private int mSpanBackgroundColor = 0xFFE0E0E0;    public CommentTextView(Context context) {        super(context);        init();    }    public CommentTextView(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        init();    }    public CommentTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init() {        setMovementMethod(LinkMovementMethod.getInstance());    }    public void setSpanClickBackground(int backgroundColor) {        this.mSpanBackgroundColor = backgroundColor;    }    public void setSpan(int start, int end, int color, boolean textBold, OnClickListener listener) {        CommentClickableSpan commentClickableSpan = new CommentClickableSpan(color, textBold, listener);        setSpan(start, end, commentClickableSpan);    }    public void setSpan(int start, int end, CommentClickableSpan span) {        CharSequence text = getText();        if (TextUtils.isEmpty(text)) {            return;        }        start = Math.max(0, start);        end = Math.min(text.length(), end);        Spannable buffer;        if (text instanceof SpannableString) {            buffer = (Spannable) text;        } else {            buffer = new SpannableString(text);        }        buffer.setSpan(span, start, end, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE);        setText(buffer);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        Object text = getText();        if (text instanceof Spannable) {            Spannable buffer = (Spannable) text;            int action = event.getAction();            if (action == MotionEvent.ACTION_UP                    || action == MotionEvent.ACTION_DOWN) {                int x = (int) event.getX();                int y = (int) event.getY();                x -= getTotalPaddingLeft();                y -= getTotalPaddingTop();                x += getScrollX();                y += getScrollY();                Layout layout = getLayout();                int line = layout.getLineForVertical(y);                int off = layout.getOffsetForHorizontal(line, x);                ClickableSpan[] link = buffer.getSpans(off, off, CommentClickableSpan.class);                if (link.length != 0) {                    if (action == MotionEvent.ACTION_UP) {                        buffer.setSpan(new BackgroundColorSpan(0x00000000), buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);                        link[0].onClick(this);                    } else if (action == MotionEvent.ACTION_DOWN) {                        buffer.setSpan(new BackgroundColorSpan(mSpanBackgroundColor), buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);                        Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]));                    }                    return true;                }            }        }        return super.onTouchEvent(event);    }    public static class CommentClickableSpan extends ClickableSpan {        private int mShowColor;        private boolean mTextBold;        private OnClickListener onClickListener;        public CommentClickableSpan(int color, boolean textBold) {            this.mShowColor = color;            this.mTextBold = textBold;        }        public CommentClickableSpan(int color, boolean textBold, OnClickListener listener) {            this.mShowColor = color;            this.mTextBold = textBold;            this.onClickListener = listener;        }        @Override        public void onClick(View widget) {            //建议使用不带OnClickListener的构造,并添加带自己的业务参数的构造  Router.gotoUserCenterActivity(uid);            if (onClickListener != null) {                onClickListener.onClick(widget);            }        }        @Override        public void updateDrawState(TextPaint ds) {            ds.setColor(mShowColor);            if (mTextBold) {                ds.setFlags(TextPaint.FAKE_BOLD_TEXT_FLAG);            }            ds.setUnderlineText(false);        }    }}复制代码

设置局部点击时的位置:setSpan()

设置局部按压时背景色:setSpanClickBackground();

转载于:https://juejin.im/post/5b8a312851882542fd2394c7

你可能感兴趣的文章
接口与抽象类的使用选择
查看>>
if __name__ == '__main__'
查看>>
CF 375D. Tree and Queries【莫队 | dsu on tree】
查看>>
Maven最佳实践 划分模块 配置多模块项目 pom modules
查看>>
Hadoop学习笔记——WordCount
查看>>
Unity应用架构设计(4)——设计可复用的SubView和SubViewModel(Part 1)
查看>>
Java-Spring-获取Request,Response对象
查看>>
opencv项目报错_pFirstBlock==pHead解决办法
查看>>
MySQL日志
查看>>
Oracle性能优化之Oracle里的执行计划
查看>>
电脑如何连接远程服务器?听语音
查看>>
使用Xcode 查看objective-C的汇编代码
查看>>
Vue.js——60分钟快速入门
查看>>
设计模式 - 模板方法模式(template method pattern) 具体解释
查看>>
mysql判断一个字符串是否包含某子串 【转】
查看>>
a bad dream
查看>>
FD_CLOEXEC用法及原因_转
查看>>
element UI 的学习一,路由跳转
查看>>
RabbitMQ三种Exchange模式(fanout,direct,topic)的性能比较
查看>>
Spring JavaBean属性值的注入方式( 属性注入, 特殊字符注入 <![CDATA[ 带有特殊字符的值 ]]> , 构造器注入 )...
查看>>