美文网首页
Android EditText中Span文本点击选中交互实战

Android EditText中Span文本点击选中交互实战

作者: 滚滚大人 | 来源:发表于2021-05-23 20:00 被阅读0次

    需求描述:这是一个类似新浪微博发帖加话题/@好友功能中的一个小交互,不是如何实现话题功能,而是讲一下点击Span文本选中的交互实现,点击Span选中和EditText的点击、长按事件冲突如何解决。
    实现过程:
    咋一看好像这个交互也太简单了,类似这样

                ClickableSpan clickableSpan = new ClickableSpan() {
                    @Override
                    public void onClick(View widget) {
                        LogUtils.e("-->>点击了话题");
                        widget.post(new Runnable() {
                            @Override
                            public void run() {
                                // 这里不能使用 start 和 end,因为选中再进行输入操作时,有可能会改变range的的范围
                                setSelection(range.getFrom(), range.getTo());
                                LogUtils.e("-->>选中 start=" + start + ",end=" + end);
                            }
                        });
                    }
    
                    @Override
                    public void updateDrawState(@NonNull TextPaint ds) {
                        ds.setUnderlineText(false);
                    }
                };
            builder.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
      richEt.setText(builder);
      richEt.setMovementMethod(LinkMovementMethod.getInstance());
    

    运行起来,你就会发现一堆坑,比如,ClickableSpan 与Edittext自身的点击事件一起触发等等。
    网上搜索了一波发现,太多大佬遇到这个问题了,解决方式也很成熟,代码如下

    public class LinkTouchMethod implements View.OnTouchListener {
        long longClickDelay = ViewConfiguration.getLongPressTimeout();
        long startTime = 0;
    
        /**
         * 需要判断一下,如果触发了
         *
         * @param v
         * @param event
         * @return
         */
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            int action = event.getAction();
            if (action == MotionEvent.ACTION_DOWN) {
                startTime = System.currentTimeMillis();
            }
            TextView tv = (TextView) v;
            CharSequence text = tv.getText();
            if (text instanceof Spanned) {
                if (action == MotionEvent.ACTION_UP) {
                    // 避免长按和点击冲突,如果超过400毫秒,认为是在长按,不执行点击操作
                    if (System.currentTimeMillis() - startTime > longClickDelay) {
                        LogUtils.e("-->>认为是长按");
                        return false;
                    }
                    int x = (int) event.getX();
                    int y = (int) event.getY();
    
                    x -= tv.getTotalPaddingLeft();
                    y -= tv.getTotalPaddingTop();
    
                    x += tv.getScrollX();
                    y += tv.getScrollY();
                    LogUtils.e("-->>y=" + y + " ," + event.toString());
                    Layout layout = tv.getLayout();
                    // 获取y坐标所在行数
                    int line = layout.getLineForVertical(y);
                    LogUtils.e("-->>line=" + line);
                    // 获取所在行数 x坐标的偏移量
                    int off = layout.getOffsetForHorizontal(line, x);
                    ClickableSpan[] link = ((Spanned) text).getSpans(off, off, ClickableSpan.class);
                    if (link.length != 0) {
                        if (x < layout.getLineWidth(line) && x > 0) {
                            link[0].onClick(tv);
                            // 需要拦截view本身的点击事件
                            return true;
                        }
                    }
                }
            }
            LogUtils.e("-->>没有处理onTouch " + action);
            return false;
        }
    }
    
    // 设置触摸监听
    richEt.setOnTouchListener(new LinkTouchMethod());
    

    这里复写了一个OnTouchListener ,然后给EditText添加触摸监听。
    跑起来,发现好像也点击选中,不过让人吐血的是,EditText的长按监听也触发了。搜索了半天没找到原因,迫不得已去看源码,最后在事件分发的源码里面发现了问题。

      public boolean dispatchTouchEvent(MotionEvent event) {
        ...
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
    
                if (!result && onTouchEvent(event)) {
                    result = true;
                }
        ...
      }
    

    当我们在LinkTouchMethod的onTouch里面对up事件返回true进行消费时,这里result会被赋值为true,根本就不会走到onTouchEvent里面,不管是点击还是长按、长按取消都是在onTouchEvent里面进行的,所以长按没有被取消,就会在发出Span点击的时候也会触发长按监听。
    到了这里ClickableSpan实现不了,OnTouchListener也不能用,没辙,只能保存up事件,自己在EditText的点击事件里面去进行span的点击触发,根据条件选择触发SpanClick或者EditText的onClick。
    可以实现GestureDetector在onSingleTapUp中捕获up事件,或者重写EditText在onTouchEvent中保存up事件,我项目里使用复写onTouchEvent比较好。

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int action = event.getActionMasked();
            if (action == MotionEvent.ACTION_UP) {
                // 必须保存MotionEvent副本,因为这个会被回收
                this.upMotionEvent = MotionEvent.obtain(event);
            }
            /*if (gestureDetector != null) {
                gestureDetector.onTouchEvent(event);
            }*/
            return super.onTouchEvent(event);
        }
    
            setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                   // 正常点击事件
                    .......
                    // 保存的up事件
                    if (upMotionEvent != null) {
                    // 通过up事件去匹配span,并响应点击
                        SpanClickHelper.spanClickHandle(MentionEditText.this, upMotionEvent);
                        upMotionEvent = null;
                    }
                }
            });
    

    这里也有个坑,MotionEvent分发使用完会被回收,尼玛我说怎么好多点击匹配不到Span。
    最后,点击、长按都可以选中Span文本。
    收工,看msi去了!

    demo地址:Study_Demo/Study_RichText at master · zombiu/Study_Demo (github.com)

    感谢:
    Android仿微博@好友,#话题#及links处理方案_BoBoMEe-CSDN博客
    安卓 TextView 七宗罪_陈蒙的博客-CSDN博客
    https://yangqiuyan.github.io/2018/11/21/LinkMovementMethod/

    相关文章

      网友评论

          本文标题:Android EditText中Span文本点击选中交互实战

          本文链接:https://www.haomeiwen.com/subject/fbmzjltx.html