Long Click on Clickable span not firing until click is released

3.8k views Asked by At

I need a clickable span to have both a normal click and a long click methods in my app and i found out here (In Android - How can I register only long clicks using a ClickableSpan) that i could extend the LinkMovementMethod class and the ClickableSpan class to allow to do that but currently the long and short clicks both work but for a long click instead of firing the long click action when the item has been pressed long enough it will wait until you release the item to fire. Here is my code for the extended classes:

LinkMovementClass

import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.view.MotionEvent;
import android.widget.TextView;

public class LongClickLinkMovementMethod extends LinkMovementMethod {

private Long lastClickTime = 0l;
private int lastX = 0;
private int lastY = 0;
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer,
                            MotionEvent event) {
    int action = event.getAction();

    if (action == MotionEvent.ACTION_UP ||
            action == MotionEvent.ACTION_DOWN) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        lastX = x;
        lastY = y;
        int deltaX = Math.abs(x-lastX);
        int deltaY = Math.abs(y-lastY);

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

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

        if (link.length != 0) {
            if (action == MotionEvent.ACTION_UP) {
                if (System.currentTimeMillis() - lastClickTime < 1000)
                    link[0].onClick(widget);
                else if (deltaX < 10 && deltaY < 10)
                    link[0].onLongClick(widget);
            } else if (action == MotionEvent.ACTION_DOWN) {
                Selection.setSelection(buffer,
                        buffer.getSpanStart(link[0]),
                        buffer.getSpanEnd(link[0]));
                lastClickTime = System.currentTimeMillis();
            }
            return true;
        }
    }

    return super.onTouchEvent(widget, buffer, event);
}


public static MovementMethod getInstance() {
    if (sInstance == null)
        sInstance = new LongClickLinkMovementMethod();

    return sInstance;
}
    private static LongClickLinkMovementMethod sInstance;
}

LongClickableSpanClass

import android.text.style.ClickableSpan;
import android.view.View;

public abstract class LongClickableSpan extends ClickableSpan {

    abstract public void onLongClick(View view);
}

Actual implementation

LongClickableSpan eruptionText = new LongClickableSpan() {

                                @Override
                                public void onClick(View tvEruptions) {
                                    LinkFunctions.link_eruption_detail(getApplicationContext(),PostErupionID);
                                }

                                @Override
                                public void onLongClick(View tvEruptions) {
                                    if(SignedInUserID != 0) {
                                        DialogFragment newFragment = new Dialogs.QuickActionsDialogFragment();
                                        // Supply num input as an argument.
                                        Bundle args = new Bundle();
                                        args.putLong("eruptionID", PostErupionID);
                                        newFragment.setArguments(args);
                                        newFragment.show(getFragmentManager(), "QuickActions");
                                    }
                                }

                            };
                            ss.setSpan(eruptionText, startpos[(int) j], endpos[(int) j], Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3

There are 3 answers

1
Pedro Oliveira On

I know this question is old and probably the OP has already solved the problem but I made some tweaks on the original LongClickLinkMovementMethod to fire after the time has passed instead of when we release the tap:

import android.os.Handler;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.view.MotionEvent;
import android.widget.TextView;


public class LongClickLinkMovementMethod extends LinkMovementMethod {

    private Handler mLongClickHandler;
    private static int LONG_CLICK_TIME = 1000;
    private boolean mIsLongPressed = false;

    @Override
    public boolean onTouchEvent(final TextView widget, Spannable buffer,
                                MotionEvent event) {
        int action = event.getAction();

        if(action == MotionEvent.ACTION_CANCEL){
            if(mLongClickHandler!=null){
                mLongClickHandler.removeCallbacksAndMessages(null);
            }
        }

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

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

            if (link.length != 0) {
                if (action == MotionEvent.ACTION_UP) {
                    if(mLongClickHandler!=null){
                        mLongClickHandler.removeCallbacksAndMessages(null);
                    }
                    if(!mIsLongPressed) {
                        link[0].onClick(widget);
                    }
                    mIsLongPressed = false;
                } else {
                    Selection.setSelection(buffer,
                            buffer.getSpanStart(link[0]),
                            buffer.getSpanEnd(link[0]));
                    mLongClickHandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            link[0].onLongClick(widget);
                            mIsLongPressed = true;
                        }
                    },LONG_CLICK_TIME);
                }
                return true;
            }
        }

        return super.onTouchEvent(widget, buffer, event);
    }


    public static MovementMethod getInstance() {
        if (sInstance == null) {
            sInstance = new LongClickLinkMovementMethod();
            sInstance.mLongClickHandler = new Handler();
        }

        return sInstance;
    }
    private static LongClickLinkMovementMethod sInstance;
}

So basicaly this uses an Handler to fire the long click event after 1000ms

0
kylin17 On

The simple way is to use View.setTag.

Code example:

tv.setOnLongClickListener(new OnLongClickListener() {

    @Override
    public boolean onLongClick(View v) {
        Toast.makeText(getApplicationContext(), "onLongClick", Toast.LENGTH_SHORT).show();
        v.setTag("Test");
        return true;
    }
});

private static class NonLongClickableUrlSpan extends URLSpan {

    public NonLongClickableUrlSpan(String url) {
        super(url);
    }

    @Override
    public void onClick(View widget) {
        if (widget.getTag() != null) {
            widget.setTag(null);
            return;
        }
        super.onClick(widget);
    }
}
0
AnT On

I edited Pedro Oliveira solution with slightly up-to-date Kotlin syntax:

class LongClickLinkMovementMethod : LinkMovementMethod() {
    private var longClickHandler: Handler? = null
    private var isLongPressed = false
    override fun onTouchEvent(
        widget: TextView, buffer: Spannable,
        event: MotionEvent
    ): Boolean {
        val action = event.action
        if (action == MotionEvent.ACTION_CANCEL) {
            longClickHandler?.removeCallbacksAndMessages(null)
        }
        if (action == MotionEvent.ACTION_UP ||
            action == MotionEvent.ACTION_DOWN
        ) {
            var x = event.x.toInt()
            var y = event.y.toInt()
            x -= widget.totalPaddingLeft
            y -= widget.totalPaddingTop
            x += widget.scrollX
            y += widget.scrollY
            val layout = widget.layout
            val line = layout.getLineForVertical(y)
            val off = layout.getOffsetForHorizontal(line, x.toFloat())
            val link = buffer.getSpans(
                off, off,
                LongClickableSpan::class.java
            )
            if (link.isNotEmpty()) {
                if (action == MotionEvent.ACTION_UP) {
                    longClickHandler?.removeCallbacksAndMessages(null)
                    if (!isLongPressed) {
                        link[0].onClick(widget)
                    }
                    isLongPressed = false
                } else {
                    Selection.setSelection(
                        buffer,
                        buffer.getSpanStart(link[0]),
                        buffer.getSpanEnd(link[0])
                    )
                    longClickHandler?.postDelayed({
                        link[0].onLongClick(widget)
                        isLongPressed = true
                    }, LONG_CLICK_TIME)
                }
                return true
            }
        }
        return super.onTouchEvent(widget, buffer, event)
    }

    companion object {
        private const val LONG_CLICK_TIME = 500L
        val instance: MovementMethod?
            get() {
                if (sInstance == null) {
                    sInstance = LongClickLinkMovementMethod()
                    // Handler deprecated https://stackoverflow.com/a/62477706/4116924
                    sInstance!!.longClickHandler = Handler(Looper.getMainLooper())
                }
                return sInstance
            }

        private var sInstance: LongClickLinkMovementMethod? = null
    }
}