美文网首页
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