Skip to content Skip to sidebar Skip to footer

Android: Clickablespan In Clickable Textview

I have a textview that can contain clickable links. When one of this links is clicked, I want to start an activity. This works fine, but it should also be possible to click the who

Solution 1:

Found a workaround that is quite straight forward. Define ClickableSpan on all the text areas that are not part of the links and handle the click on them as if the text view was clicked:

TextViewtv= (TextView)findViewById(R.id.textview01);      
Spannablespan= Spannable.Factory.getInstance().newSpannable("test link span");   
span.setSpan(newClickableSpan() {  
    @OverridepublicvoidonClick(View v) {  
        Log.d("main", "link clicked");
        Toast.makeText(Main.this, "link clicked", Toast.LENGTH_SHORT).show(); 
    } }, 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

// All the rest will have the same spannable.ClickableSpancs=newClickableSpan() {  
    @OverridepublicvoidonClick(View v) {  
        Log.d("main", "textview clicked");
        Toast.makeText(Main.this, "textview clicked", Toast.LENGTH_SHORT).show(); 
    } };

// set the "test " spannable.
span.setSpan(cs, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

// set the " span" spannable
span.setSpan(cs, 6, span.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

tv.setText(span);

tv.setMovementMethod(LinkMovementMethod.getInstance());

Hope this helps (I know this thread is old, but in case anyone sees it now...).

Solution 2:

This is a quite easy solution.. This worked for me

textView.setOnClickListener(newView.OnClickListener() {
    @OverridepublicvoidonClick(View v) {
        ClassroomLog.log(TAG, "Textview Click listener ");
        if (textView.getSelectionStart() == -1 && textView.getSelectionEnd() == -1) {
            // do your code here this will only call if its not a hyperlink
        }
    }
});

Solution 3:

Matthew suggested subclassing TextView and with that hint a came up with a rather ugly workaround. But it works:

I've created a "ClickPreventableTextView" which I use when I have clickablespans in a TextView that should be clickable as a whole.

In its onTouchEvent method this class calls the onTouchEvent method of MovementMethod before calling onTouchEvent on its base TextView class. So it is guaranted, that the Listener of the clickablespan will be invoked first. And I can prevent invoking the OnClickListener for the whole TextView

/**
 * TextView that allows to insert clickablespans while whole textview is still clickable<br>
 * If a click an a clickablespan occurs, click handler of whole textview will <b>not</b> be invoked
 * In your span onclick handler you first have to check whether {@link ignoreSpannableClick} returns true, if so just return from click handler
 * otherwise call {@link preventNextClick} and handle the click event
 * @author Lukas
 *
 */publicclassClickPreventableTextViewextendsTextViewimplementsOnClickListener {
privateboolean preventClick;
private OnClickListener clickListener;
privateboolean ignoreSpannableClick;

publicClickPreventableTextView(Context context) {
    super(context);
}

publicClickPreventableTextView(Context context, AttributeSet attrs) {
    super(context, attrs);
}

publicClickPreventableTextView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
}

publicbooleanonTouchEvent(MotionEvent event) {
    if (getMovementMethod() != null)
        getMovementMethod().onTouchEvent(this, (Spannable)getText(), event);
    this.ignoreSpannableClick = true;
    booleanret=super.onTouchEvent(event);
    this.ignoreSpannableClick = false;
    return ret;
}

/**
 * Returns true if click event for a clickable span should be ignored
 * @return true if click event should be ignored
 */publicbooleanignoreSpannableClick() {
    return ignoreSpannableClick;
}

/**
 * Call after handling click event for clickable span
 */publicvoidpreventNextClick() {
    preventClick = true;
}

@OverridepublicvoidsetOnClickListener(OnClickListener listener) {
    this.clickListener = listener;
    super.setOnClickListener(this);
}

@OverridepublicvoidonClick(View v) {
    if (preventClick) {
        preventClick = false;
    } elseif (clickListener != null)
        clickListener.onClick(v);
}
}

The listener for the clickable span now looks like that

    span.setSpan(newClickableSpan() {  
        @OverridepublicvoidonClick(View v) {  
            Log.d("main", "link clicked");
            if (widget instanceof ClickPreventableTextView) {
                if (((ClickPreventableTextView)widget).ignoreSpannableClick())
                    return;
                ((ClickPreventableTextView)widget).preventNextClick();
            }

            Toast.makeText(Main.this, "link clicked", Toast.LENGTH_SHORT).show(); 
        } }, 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

For me the main disadvantage is, that now getMovementMethod().onTouchEvent will be called twice (TextView calls that method in it's onTouchEvent method). I don't know if this has any side effects, atm it works as expected.

Solution 4:

The code is work for me and that is from source code of LinkMovementMethod

tv.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                TextView tv = (TextView) v;
                if (event.action == MotionEvent.ACTION_UP) {
                    int x = (int) event.getX();
                    int y = (int) event.getY();

                    Layout layout = tv.getLayout();
                    int line = layout.getLineForVertical(y);
                    int off = layout.getOffsetForHorizontal(line, x);

                    ClickableSpan[] link = contentSpan.getSpans(off, off, ClickableSpan.class);

                    if (link.length != 0) {
                        link[0].onClick(tv);
                    } else {
                       //do other click
                    }
                }
                returntrue;
            }
        });

Solution 5:

Solved something very similar in a very nice way. I wanted to have text that has a link which is clickable!! and i wanted to be able to press the text Where there is no link and have a on click listener in it. I took the LinkMovementMethod from grepcode and changed it a little Copy and past this class and copy the bottom and it will work :

import android.text.Layout;
import android.text.NoCopySpan;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.MovementMethod;
import android.text.method.ScrollingMovementMethod;
import android.text.style.ClickableSpan;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

publicclassCustomLinkMovementMethodextendsScrollingMovementMethod
{
    privatestaticfinalintCLICK=1;
    privatestaticfinalintUP=2;
    privatestaticfinalintDOWN=3;

publicabstractinterfaceTextClickedListener {
    publicabstractvoidonTextClicked();
}
TextClickedListenerlistener=null;
publicvoidsetOnTextClickListener(TextClickedListener listen){
    listener = listen;
}
@OverridepublicbooleanonKeyDown(TextView widget, Spannable buffer,
                         int keyCode, KeyEvent event) {
    switch (keyCode) {
        case KeyEvent.KEYCODE_DPAD_CENTER:
        case KeyEvent.KEYCODE_ENTER:
            if (event.getRepeatCount() == 0) {
                if (action(CLICK, widget, buffer)) {
                    returntrue;
                }
            }
    }

    returnsuper.onKeyDown(widget, buffer, keyCode, event);
}

@Overrideprotectedbooleanup(TextView widget, Spannable buffer) {
    if (action(UP, widget, buffer)) {
        returntrue;
    }

    returnsuper.up(widget, buffer);
}

@Overrideprotectedbooleandown(TextView widget, Spannable buffer) {
    if (action(DOWN, widget, buffer)) {
        returntrue;
    }

    returnsuper.down(widget, buffer);
}

@Overrideprotectedbooleanleft(TextView widget, Spannable buffer) {
    if (action(UP, widget, buffer)) {
        returntrue;
    }

    returnsuper.left(widget, buffer);
}

@Overrideprotectedbooleanright(TextView widget, Spannable buffer) {
    if (action(DOWN, widget, buffer)) {
        returntrue;
    }

    returnsuper.right(widget, buffer);
}

privatebooleanaction(int what, TextView widget, Spannable buffer) {
    booleanhandled=false;

    Layoutlayout= widget.getLayout();

    intpadding= widget.getTotalPaddingTop() +
            widget.getTotalPaddingBottom();
    intareatop= widget.getScrollY();
    intareabot= areatop + widget.getHeight() - padding;

    intlinetop= layout.getLineForVertical(areatop);
    intlinebot= layout.getLineForVertical(areabot);

    intfirst= layout.getLineStart(linetop);
    intlast= layout.getLineEnd(linebot);

    ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);

    inta= Selection.getSelectionStart(buffer);
    intb= Selection.getSelectionEnd(buffer);

    intselStart= Math.min(a, b);
    intselEnd= Math.max(a, b);

    if (selStart < 0) {
        if (buffer.getSpanStart(FROM_BELOW) >= 0) {
            selStart = selEnd = buffer.length();
        }
    }

    if (selStart > last)
        selStart = selEnd = Integer.MAX_VALUE;
    if (selEnd < first)
        selStart = selEnd = -1;

    switch (what) {
        case CLICK:
            if (selStart == selEnd) {
                returnfalse;
            }

            ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class);

            if (link.length != 1)
                returnfalse;

            link[0].onClick(widget);
            break;

        case UP:
            int beststart, bestend;

            beststart = -1;
            bestend = -1;

            for (inti=0; i < candidates.length; i++) {
                intend= buffer.getSpanEnd(candidates[i]);

                if (end < selEnd || selStart == selEnd) {
                    if (end > bestend) {
                        beststart = buffer.getSpanStart(candidates[i]);
                        bestend = end;
                    }
                }
            }

            if (beststart >= 0) {
                Selection.setSelection(buffer, bestend, beststart);
                returntrue;
            }

            break;

        case DOWN:
            beststart = Integer.MAX_VALUE;
            bestend = Integer.MAX_VALUE;

            for (inti=0; i < candidates.length; i++) {
                intstart= buffer.getSpanStart(candidates[i]);

                if (start > selStart || selStart == selEnd) {
                    if (start < beststart) {
                        beststart = start;
                        bestend = buffer.getSpanEnd(candidates[i]);
                    }
                }
            }

            if (bestend < Integer.MAX_VALUE) {
                Selection.setSelection(buffer, beststart, bestend);
                returntrue;
            }

            break;
    }

    returnfalse;
}

publicbooleanonKeyUp(TextView widget, Spannable buffer,
                       int keyCode, KeyEvent event) {
    returnfalse;
}

@OverridepublicbooleanonTouchEvent(TextView widget, Spannable buffer,
                            MotionEvent event) {
    intaction= event.getAction();

    if (action == MotionEvent.ACTION_UP ||
            action == MotionEvent.ACTION_DOWN) {
        intx= (int) event.getX();
        inty= (int) event.getY();

        x -= widget.getTotalPaddingLeft();
        y -= widget.getTotalPaddingTop();

        x += widget.getScrollX();
        y += widget.getScrollY();

        Layoutlayout= widget.getLayout();
        intline= layout.getLineForVertical(y);
        intoff= layout.getOffsetForHorizontal(line, x);

        ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);

        if (link.length != 0) {
            if (action == MotionEvent.ACTION_UP) {
                link[0].onClick(widget);
            } elseif (action == MotionEvent.ACTION_DOWN) {
                Selection.setSelection(buffer,
                        buffer.getSpanStart(link[0]),
                        buffer.getSpanEnd(link[0]));
            }

            returntrue;
        } else {
            Selection.removeSelection(buffer);

            if (action == MotionEvent.ACTION_UP) {
                if(listener != null)
                    listener.onTextClicked();
            }
        }
    }

    returnsuper.onTouchEvent(widget, buffer, event);
}





publicvoidinitialize(TextView widget, Spannable text) {
    Selection.removeSelection(text);
    text.removeSpan(FROM_BELOW);
}

publicvoidonTakeFocus(TextView view, Spannable text, int dir) {
    Selection.removeSelection(text);

    if ((dir & View.FOCUS_BACKWARD) != 0) {
        text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
    } else {
        text.removeSpan(FROM_BELOW);
    }
}

publicstatic MovementMethod getInstance() {
    if (sInstance == null)
        sInstance = newCustomLinkMovementMethod();

    return sInstance;
}

privatestatic CustomLinkMovementMethod sInstance;
privatestaticObjectFROM_BELOW=newNoCopySpan.Concrete();

}

Then in your code where the text view is add:

CustomLinkMovementMethodlink= (CustomLinkMovementMethod)CustomLinkMovementMethod.getInstance();
        link.setOnTextClickListener(newCustomLinkMovementMethod.TextClickedListener() {
            @OverridepublicvoidonTextClicked() {
                Toast.makeText(UserProfileActivity.this, "text Pressed", Toast.LENGTH_LONG).show();

            }
        });
        YOUR_TEXTVIEW.setMovementMethod(link);

Post a Comment for "Android: Clickablespan In Clickable Textview"