Android 进阶学习(二十三) TextView 添加ClickableSpan的故事

在Android 里面,想要实现一段文字中部分文字可以点击就可以使用ClickableSpan,大概的方式

       tv = (TextView) findViewById(R.id.tv_tsm_test);
       SpannableStringBuilder builder = new SpannableStringBuilder("这个是连接");
       builder.setSpan(new ClickableSpan() {
           @Override
           public void onClick(@NonNull View widget) {
               tv.setBackgroundColor(Color.GREEN);
           }
       }, 3, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
       tv.setText(builder);
       tv.setMovementMethod(LinkMovementMethod.getInstance());
       tv.setAutoLinkMask(Linkify.WEB_URLS);

实现的效果如下


image.png

点击连接这两个字就可以回调ClickableSpan 的 onClick方法将背景变为绿色,一个非常简单的应用, 现在产品又提出需求,要求我们给这个TextView 添加一个长按的事件, 心想这么简单的,几行代码就能实现,回去修改代码

       tv = (TextView) findViewById(R.id.tv_tsm_test);
       SpannableStringBuilder builder = new SpannableStringBuilder("这个是连接");
       builder.setSpan(new ClickableSpan() {
           @Override
           public void onClick(@NonNull View widget) {
               tv.setBackgroundColor(Color.GREEN);
           }
       }, 3, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
       tv.setText(builder);
       tv.setMovementMethod(LinkMovementMethod.getInstance());
       tv.setAutoLinkMask(Linkify.WEB_URLS);
       tv.setOnLongClickListener(new View.OnLongClickListener() {
           @Override
           public boolean onLongClick(View v) {
               tv.setBackgroundColor(Color.RED);
               return true;
           }
       });
GIF 2021-5-12 13-37-27.gif

发现如果这个长按事件如果是在ClickableSpan上面响应的时候,同时也会回调ClickableSpan 的onClick事件,
这种情况是由LinkMovementMethod 导致的问题,查看他的源码,在onTouch中发现问题

 @Override
   public boolean onTouchEvent(TextView widget, Spannable buffer,
                               MotionEvent event) {
       int action = event.getAction();
       if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
       ......
           ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
           if (links.length != 0) {
               ClickableSpan link = links[0];
               if (action == MotionEvent.ACTION_UP) {///只判断了抬起事件,没有判断时长
                   if (link instanceof TextLinkSpan) {
                       ((TextLinkSpan) link).onClick(
                               widget, TextLinkSpan.INVOCATION_METHOD_TOUCH);
                   } else {
                       link.onClick(widget);
                   }
               } else if (action == MotionEvent.ACTION_DOWN) {
                  ........
               }
               return true;
           } else {
               Selection.removeSelection(buffer);
           }
       }
       return super.onTouchEvent(widget, buffer, event);
   }

发现在响应事件的地方判断只要是抬起事件,不管这个事件点击还是长按,他都会响应ClickableSpan 的onClick事件,我们需要修改一下这个方法,给定一个时长,当超过这个时长,就不响应点击事件

private static final long CLICK_DELAY = 1*1000;

    if (link.length != 0) {
               switch (action){
                   case MotionEvent.ACTION_UP:
                       long flag=(System.currentTimeMillis() - lastClickTime);
                       if (flag< CLICK_DELAY) {
                           link[0].onClick(widget);
                       }
                       return true;
                   case MotionEvent.ACTION_DOWN:
                       Selection.setSelection(buffer,
                               buffer.getSpanStart(link[0]),
                               buffer.getSpanEnd(link[0]));
                       lastClickTime = System.currentTimeMillis();
                       return true;
               }
           } else {
               Selection.removeSelection(buffer);
           }

修改后的代码变成了这个样子,这次我们再来重新试一下,发现不会在长按的时候响ClickableSpan 的onClick事件了,你以为这样就结束了吗,并没有 产品又来了一个牛X的操作,他觉得长按复制要将所有文本都复制下来,她想要自由复制,这个长按复制体验并不好,她不想要了


扎心了老铁.png

在android 中想要实现TextView的复制功能其实也并不复杂,只需要将

       android:textIsSelectable="true"

这个属性设置为true 就可以了,但是测试的时候发现 ,在点击ClickableSpan 的时候,会调用两次,
what ? 为什么会导致这个问题,我明明在LinkMovementMethod 的onTouch 里面才刚看到过,只有一个点击事件,为什么会响应两次,
经过了一番折腾后发现在TextView onTouchEvent 里面也有相应的判断, 代码案例如下

 ///抬起操作,并且有焦点
 final boolean touchIsFinished = (action == MotionEvent.ACTION_UP)
               && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
       //enable 并且text 是Spannable  
       if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
               && mText instanceof Spannable && mLayout != null) {
           boolean handled = false;
           if (mMovement != null) {
               handled |= mMovement.onTouchEvent(this, mSpannable, event);
           }
           final boolean textIsSelectable = isTextSelectable();
           ///意思是抬起操作并且有焦点 并且  链接可以被点击并且设置过setAutoLinkMask 这个属性  同时 textIsSelectable=true
           ///在所有的条件都满足的情况下,就会调用ClickableSpan 的onClick事件,
           if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
               // The LinkMovementMethod which should handle taps on links has not been installed
               // on non editable text that support text selection.
               // We reproduce its behavior here to open links for these.
               ClickableSpan[] links = mSpannable.getSpans(getSelectionStart(),
                   getSelectionEnd(), ClickableSpan.class);
               if (links.length > 0) {
                   links[0].onClick(this);
                   handled = true;
               }
           }

实际项目中,由于代码是祖传的,修改的时候不能修改比较重要的属性,所以不能将原来的setAutoLinkMask(Linkify.WEB_URLS);这个属性去掉,所以只能修改这个mLinksClickable ,让他不满足情况,就不会影响我们的事件了,在实际开发过程中也可以将setMovementMethod(LinkMovementMethod.getInstance()); 这段代码移除,或者尝试移除setAutoLinkMask(Linkify.WEB_URLS); 这段代码,我选择的是

       android:linksClickable="false"

在布局中添加这个属性,就可以达到我们想要的效果了,点击事件只会响应一次了,
但是在实际开发过程中,我修改的代码是在组件里面,使用这个组件的的应用有好几个,那么就要根据功能开关动态的在代码中去修改这些属性
实际项目中代码为

 SpannableStringBuilder builder = SpannableUtil.addInnerLink(vh.tv, msg, color, needLinkUnderLine, new           
       SpannableUtil.LinkCallback() {
           @Override
           public void onLinkClick(String originText, String link, int startIndex, int endIndex) {

           }
       });
 vh.tv.setText(SpannableUtil.addPhoneLink(msg.getMsgContent(), builder, mExtAdapter.isAddPhoneLink(), color, new 
     SpannableUtil.LinkCallback() {
           @Override
           public void onLinkClick(String originText, String link, int startIndex, int endIndex) {
               
           }
       }), TextView.BufferType.SPANNABLE);

  if(打开自由复制){
     vh.tv.setLinksClickable(false);
     vh.tv.setTextIsSelectable(true);
 }

这段代码看起来没有什么问题,但是在部分机型中发现有概率会让ClickableSpan 的onClick事件的回调不调用,真是让这个ClickableSpan 给我狠狠的教育了一顿,问题一个接着一个,没办法只能继续去分析,
查看TextView setTextIsSelectable 方法

  public void setTextIsSelectable(boolean selectable) {
       if (!selectable && mEditor == null) return; // false is default value with no edit data

       createEditorIfNeeded();
       if (mEditor.mTextIsSelectable == selectable) return;

       mEditor.mTextIsSelectable = selectable;
       setFocusableInTouchMode(selectable);
       setFocusable(FOCUSABLE_AUTO);
       setClickable(selectable);
       setLongClickable(selectable);

       // mInputType should already be EditorInfo.TYPE_NULL and mInput should be null

       setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
       setText(mText, selectable ? BufferType.SPANNABLE : BufferType.NORMAL);

       // Called by setText above, but safer in case of future code changes
       mEditor.prepareCursorControllers();
   }

重点是 setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null); 这个地方,如果设置的是可以自由复制,那么就使用ArrowKeyMovementMethod ,否则将MovementMethod 设置为null ,将我们的LinkMovementMethod给替换掉了,所以将 vh.tv.setTextIsSelectable(true);这个方法提前到祖传代码setMovementMethod 之前,问题得到解决

image.png

你可能感兴趣的:(Android 进阶学习(二十三) TextView 添加ClickableSpan的故事)