android ClickableSpan intercepts the click event

강윤식 picture 강윤식 · May 28, 2013 · Viewed 15.5k times · Source

I have a TextView in a Layout. It's so simple. I put a OnClickListener in the layout and some part of the TextView is set to be ClickableSpan. I want the ClickableSpan to do something in the onClick function when it's clicked and when the other part of the TextView is clicked, it has to do something in the onClick functions of the OnClickListener of the layout. Here's my code.

    RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
    l.setOnClickListener(new OnClickListener(){
        @Override
        public void onClick(View v) {
            Toast.makeText(MainActivity.this, "whole layout", Toast.LENGTH_SHORT).show();
        }
    });

    TextView textView = (TextView)findViewById(R.id.t1);
    textView.setMovementMethod(LinkMovementMethod.getInstance());

    SpannableString spannableString = new SpannableString(textView.getText().toString());
    ClickableSpan span = new ClickableSpan() {
        @Override
        public void onClick(View widget) {
            Toast.makeText(MainActivity.this, "just word", Toast.LENGTH_SHORT).show();
        }
    };
    spannableString.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE);        
    textView.setText(spannableString);

Answer

Hai Zhang picture Hai Zhang · Dec 10, 2015

I've also run into this problem, and thanks to the source code @KMDev mentioned, I've came up with a much cleaner approach.

First, since I'm only having a TextView that is to be made partially clickable, in fact I don't need most of the functionalities LinkMovementMethod (and its super class ScrollingMovementMethod) which adds ability to handle key press, scrolling, etc.

Instead, create a custom MovementMethod that uses the OnTouch() code from LinkMovementMethod:

ClickableMovementMethod.java

package com.example.yourapplication;

import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.BaseMovementMethod;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.MotionEvent;
import android.widget.TextView;

/**
 * A movement method that traverses links in the text buffer and fires clicks. Unlike
 * {@link LinkMovementMethod}, this will not consume touch events outside {@link ClickableSpan}s.
 */
public class ClickableMovementMethod extends BaseMovementMethod {

    private static ClickableMovementMethod sInstance;

    public static ClickableMovementMethod getInstance() {
        if (sInstance == null) {
            sInstance = new ClickableMovementMethod();
        }
        return sInstance;
    }

    @Override
    public boolean canSelectArbitrarily() {
        return false;
    }

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {

        int action = event.getActionMasked();
        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {

            int x = (int) event.getX();
            int y = (int) event.getY();
            x -= widget.getTotalPaddingLeft();
            y -= widget.getTotalPaddingTop();
            x += widget.getScrollX();
            y += widget.getScrollY();

            Layout layout = widget.getLayout();
            int line = layout.getLineForVertical(y);
            int off = 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);
                } else {
                    Selection.setSelection(buffer, buffer.getSpanStart(link[0]),
                            buffer.getSpanEnd(link[0]));
                }
                return true;
            } else {
                Selection.removeSelection(buffer);
            }
        }

        return false;
    }

    @Override
    public void initialize(TextView widget, Spannable text) {
        Selection.removeSelection(text);
    }
}

Then using this ClickableMovementMethod, touch event will not be consumed by movement method any more. However, TextView.setMovementMethod() which calls TextView.fixFocusableAndClickableSettings() will set clickable, long-clickable and focusable to true which will make View.onTouchEvent() consume the touch event. To fix for this, simply reset the three attributes.

So the final utility method, to accompany the ClickableMovementMethod, is here:

public static void setTextViewLinkClickable(TextView textView) {
    textView.setMovementMethod(ClickableMovementMethod.getInstance());
    // Reset for TextView.fixFocusableAndClickableSettings(). We don't want View.onTouchEvent()
    // to consume touch events.
    textView.setClickable(false);
    textView.setLongClickable(false);
}

This works like a charm for me.

Click events on ClickableSpans are fired, and click outside them are passed throught to parent layout listener.

Note that if your are making your TextView selectable, I haven't tested for that case, and maybe you need to dig into the source yourself :P