Android advanced learning (XXIII) story of adding clickablespan to textview

Time:2022-5-14

In Android, if you want to realize that some text in a text can be clicked, you can use clickablespan in the approximate way

tv = (TextView) findViewById(R.id.tv_tsm_test);
       Spannablestringbuilder builder = new spannablestringbuilder ("this is a connection");
       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);

The effects are as follows

Android advanced learning (XXIII) story of adding clickablespan to textview

image.png

Click the word “connect” to call back the onclick method of clickablespan and turn the background into green. It is a very simple application. Now the product has put forward a demand and asked us to add a long press event to the textview. I thought it was so simple that a few lines of code can be realized. Go back and modify the code

tv = (TextView) findViewById(R.id.tv_tsm_test);
       Spannablestringbuilder builder = new spannablestringbuilder ("this is a connection");
       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;
           }
       });
Android advanced learning (XXIII) story of adding clickablespan to textview

GIF 2021-5-12 13-37-27.gif

It is found that if the long press event responds on the clickablespan, it will also call back the onclick event of the clickablespan,
This situation is caused by linkmovementmethod. Check its source code and find the problem in 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) {// / only the lifting event is judged, and the judgment time is not long
                   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);
   }

It is found that in response to the event, it is judged that as long as it is a lift event, no matter whether the event is clicked or long pressed, it will respond to the onclick event of clickablespan. We need to modify this method to give a time length. When it exceeds this time length, it will not respond to the click event

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);
           }

The modified code looks like this. This time, we’ll try again and find that the onclick event of clickablespan won’t sound when long pressing. Do you think it’s over? There’s another niux operation for no product. He thinks that long pressing copy should copy all the text. She wants to copy freely. The long pressing copy experience is not good, and she doesn’t want it

Android advanced learning (XXIII) story of adding clickablespan to textview

that really hurts, man. png

In fact, it is not complicated to realize the copy function of textview in Android. You only need to

       android:textIsSelectable="true"

When the clickable property is set to true, it will be found when the test is called twice,
what ? Why this problem is caused? I have just seen it in the ontouch of linkmovementmethod. Why does it respond twice when there is only one click event,
After some tossing, it is found that there are corresponding judgments in textview ontouchevent. The code case is as follows

///Lift operation with focus
 final boolean touchIsFinished = (action == MotionEvent.ACTION_UP)
               && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
       //Enable and text is 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();
           ///It means that the operation is lifted and there is focus, and the link can be clicked, and the attribute setautolinkmask has been set, and textisselectable = true
           ///When all conditions are met, the onclick event of clickablespan will be called,
           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;
               }
           }

In the actual project, because the code is ancestral, important attributes cannot be modified during modification, so the original setautolinkmask (linkify. Web_urls) cannot be modified; This attribute is removed, so only the mlinksclickable can be modified. If it is not satisfied, it will not affect our events. In the actual development process, setmovementmethod (linkmovementmethod. Getinstance()); Remove this code, or try to remove setautolinkmask (linkify. Web_urls); This is my choice

       android:linksClickable="false"

By adding this attribute to the layout, we can achieve the desired effect. The click event will respond only once,
However, in the actual development process, the code I modify is in the component. There are several applications using this component, so we need to dynamically modify these properties in the code according to the function switch
The code in the actual project is

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 (open free copy){
     vh.tv.setLinksClickable(false);
     vh.tv.setTextIsSelectable(true);
 }

There seems to be no problem with this code, but it is found in some models that there is a probability that the callback of onclick event of clickablespan will not be called. This clickablespan really taught me a lot. One problem after another, there is no way but to continue to analyze,
View the textview settextisselectable method

  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();
   }

The key point is setmovementmethod (selectable? Arrowkeymovementmethod. Getinstance(): null); In this place, if it is set that you can copy freely, use the arrowkeymovementmethod. Otherwise, set the movementmethod to null and replace our linkmovementmethod, so VH tv. setTextIsSelectable(true); This method is advanced to the ancestral code setmovementmethod, and the problem is solved

Android advanced learning (XXIII) story of adding clickablespan to textview

image.png