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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容