Android: Detect if user touches and drags out of button region?
Asked Answered
C

10

58

In Android, how can we detect if a user touches on button and drags out of region of this button?

Customhouse answered 20/6, 2011 at 11:3 Comment(0)
V
98

Check the MotionEvent.MOVE_OUTSIDE: Check the MotionEvent.MOVE:

private Rect rect;    // Variable rect to hold the bounds of the view

public boolean onTouch(View v, MotionEvent event) {
    if(event.getAction() == MotionEvent.ACTION_DOWN){
        // Construct a rect of the view's bounds
        rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());

    }
    if(event.getAction() == MotionEvent.ACTION_MOVE){
        if(!rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())){
            // User moved outside bounds
        }
    }
    return false;
}

NOTE: If you want to target Android 4.0, a whole world of new possibilities opens: http://developer.android.com/reference/android/view/MotionEvent.html#ACTION_HOVER_ENTER

Victualer answered 9/11, 2011 at 18:42 Comment(7)
Sorry but it doesn't work. I only can catch ACTION_UP, ACTION_DOWN and ACTION_MOVE but not ACTION_OUTSIDE.Customhouse
It's available from API Level 3 onwards: developer.android.com/reference/android/view/…Victualer
According to this answer (#6162997) I believe we only can use ACTION_OUTSIDE to detect if touch is outside an Activity, not a view.Customhouse
See FrostRocket's answer for an update to rect.contains test to make this work for individual views that aren't positioned at the origin.Sitology
I believe if you want to get ACTION_OUTSIDE you will need to use getActionMasked(). Or you could compared the masks instead: if ((event.getAction() & MotionEvent.ACTION_OUTSIDE) == MotionEvent.ACTION_OUTSIDE). This is because ACTION_OUTSIDE will never trigger without already having another action, so it will be masked.Acima
@ChadBingham Not receiving ACTION_OUTSIDE when dragging out of the button, either with getAction() or getActionMasked()Sumner
p.s. confirmed - ACTION_OUTSIDE doesn't work for views.Sumner
B
22

The answer posted by Entreco needed some slight tweaking in my case. I had to substitute:

if(!rect.contains((int)event.getX(), (int)event.getY()))

for

if(!rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY()))

because event.getX() and event.getY() only apply to the ImageView itself, not the entire screen.

Busey answered 4/1, 2013 at 5:46 Comment(1)
+1 thanks, that's the more universal approach and works in custom view implementations tooNava
L
13

I had this same problem as the OP whereby I wanted to know when (1) a particular View was touched down as well as either (2a) when the down touch was released on the View or (2b) when the down touch moved outside the bounds of the View. I brought together the various answers in this thread to create a simple extension of View.OnTouchListener (named SimpleTouchListener) so that others don't have to fiddle around with the MotionEvent object. The source for this class can be found here or at the bottom of this answer.

To use this class, simply set it as the parameter of the View.setOnTouchListener(View.OnTouchListener) method follows:

myView.setOnTouchListener(new SimpleTouchListener() {

    @Override
    public void onDownTouchAction() {
        // do something when the View is touched down
    }

    @Override
    public void onUpTouchAction() {
        // do something when the down touch is released on the View
    }

    @Override
    public void onCancelTouchAction() {
        // do something when the down touch is canceled
        // (e.g. because the down touch moved outside the bounds of the View
    }
});

Here is the source of the class which you are welcome to add to your project:

public abstract class SimpleTouchListener implements View.OnTouchListener {

    /**
     * Flag determining whether the down touch has stayed with the bounds of the view.
     */
    private boolean touchStayedWithinViewBounds;

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                touchStayedWithinViewBounds = true;
                onDownTouchAction();
                return true;

            case MotionEvent.ACTION_UP:
                if (touchStayedWithinViewBounds) {
                    onUpTouchAction();
                }
                return true;

            case MotionEvent.ACTION_MOVE:
                if (touchStayedWithinViewBounds
                        && !isMotionEventInsideView(view, event)) {
                    onCancelTouchAction();
                    touchStayedWithinViewBounds = false;
                }
                return true;

            case MotionEvent.ACTION_CANCEL:
                onCancelTouchAction();
                return true;

            default:
                return false;
        }
    }

    /**
     * Method which is called when the {@link View} is touched down.
     */
    public abstract void onDownTouchAction();

    /**
     * Method which is called when the down touch is released on the {@link View}.
     */
    public abstract void onUpTouchAction();

    /**
     * Method which is called when the down touch is canceled,
     * e.g. because the down touch moved outside the bounds of the {@link View}.
     */
    public abstract void onCancelTouchAction();

    /**
     * Determines whether the provided {@link MotionEvent} represents a touch event
     * that occurred within the bounds of the provided {@link View}.
     *
     * @param view  the {@link View} to which the {@link MotionEvent} has been dispatched.
     * @param event the {@link MotionEvent} of interest.
     * @return true iff the provided {@link MotionEvent} represents a touch event
     * that occurred within the bounds of the provided {@link View}.
     */
    private boolean isMotionEventInsideView(View view, MotionEvent event) {
        Rect viewRect = new Rect(
                view.getLeft(),
                view.getTop(),
                view.getRight(),
                view.getBottom()
        );

        return viewRect.contains(
                view.getLeft() + (int) event.getX(),
                view.getTop() + (int) event.getY()
        );
    }
}
Lavin answered 26/3, 2017 at 16:9 Comment(2)
this is a great answer, especially for people working with the issue for when ACTION_CANCEL not triggering as it should.Tapeworm
Yes it is a great answer!Opiumism
P
8

I added some logging in my OnTouch and found out that MotionEvent.ACTION_CANCEL was being hit. That's good enough for me...

Perdu answered 25/5, 2015 at 14:49 Comment(2)
You'll get a MotionEvent.ACTION_CANCEL callback if your View is contained within a parent ViewGroup like ScrollView which is interested in stealing move touches from its children but otherwise there's no guarantee you'll get a MotionEvent.ACTION_CANCEL callback in your View.Lavin
The only solution that worked out for me, thanks saved meMisconceive
N
3

The top 2 answers are fine except when the view is inside a scrollview: when scrolling takes place because you move your finger, it is still registered as a touch event but not as a MotionEvent.ACTION_MOVE event. So to improve the answer (which is only needed if your view is inside a scroll element):

private Rect rect;    // Variable rect to hold the bounds of the view

public boolean onTouch(View v, MotionEvent event) {
    if(event.getAction() == MotionEvent.ACTION_DOWN){
        // Construct a rect of the view's bounds
        rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());

    } else if(rect != null && !rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())){
        // User moved outside bounds
    }
    return false;
}

I tested this on Android 4.3 and Android 4.4

I haven't noticed any differences between Moritz's answer and to top 2 but this also applies for his answer:

private Rect rect;    // Variable rect to hold the bounds of the view

public boolean onTouch(View v, MotionEvent event) {
    if(event.getAction() == MotionEvent.ACTION_DOWN){
        // Construct a rect of the view's bounds
        rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());

    } else if (rect != null){
        v.getHitRect(rect);
        if(rect.contains(
                Math.round(v.getX() + event.getX()),
                Math.round(v.getY() + event.getY()))) {
            // inside
        } else {
            // outside
        }
    }
    return false;
}
Niveous answered 14/4, 2015 at 8:5 Comment(1)
I've just implemented this code on a button which is contained in an item of a RecyclerView and works perfectly. I just wanted to say that you can get rid of the new Rect on the ACTION_DOWN if you add it before getHitRect. Rect rect = new Rect(); getHitRect(rect);Maxa
D
3

Reuseable Kotlin Solution

I started with two custom extension functions:

val MotionEvent.up get() = action == MotionEvent.ACTION_UP

fun MotionEvent.isIn(view: View): Boolean {
    val rect = Rect(view.left, view.top, view.right, view.bottom)
    return rect.contains((view.left + x).toInt(), (view.top + y).toInt())
}

Then listen to touches on the view. This will only fire if ACTION_DOWN was originally on the view. When you release your finger, it will check if it was still on the view.

myView.setOnTouchListener { view, motionEvent ->
    if (motionEvent.up && !motionEvent.isIn(view)) {
        // Talk your action here
    }
    false
}
Deductible answered 31/1, 2020 at 2:13 Comment(0)
T
2
view.setClickable(true);
view.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (!v.isPressed()) {
            Log.e("onTouch", "Moved outside view!");
        }
        return false;
    }
});

view.isPressed uses view.pointInView and includes some touch slop. If you don't want slop, just copy the logic from the internal view.pointInView (which is public, but hidden so it's not a part of the official API and could disappear at any time).

view.setClickable(true);
view.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            v.setTag(true);
        } else {
            boolean pointInView = event.getX() >= 0 && event.getY() >= 0
                    && event.getX() < (getRight() - getLeft())
                    && event.getY() < (getBottom() - getTop());
            boolean eventInView = ((boolean) v.getTag()) && pointInView;
            Log.e("onTouch", String.format("Dragging currently in view? %b", pointInView));
            Log.e("onTouch", String.format("Dragging always in view? %b", eventInView));
            v.setTag(eventInView);
        }
        return false;
    }
});
Tovatovar answered 6/10, 2015 at 22:17 Comment(0)
T
1

While the answer from @FrostRocket is correct you should use view.getX() and Y to account for translations changes as well:

 view.getHitRect(viewRect);
 if(viewRect.contains(
         Math.round(view.getX() + event.getX()),
         Math.round(view.getY() + event.getY()))) {
   // inside
 } else {
   // outside
 }
Training answered 17/10, 2014 at 10:37 Comment(0)
O
1

Here is a View.OnTouchListener that you can use to see if MotionEvent.ACTION_UP was sent while the user had his/her finger outside of the view:

private OnTouchListener mOnTouchListener = new View.OnTouchListener() {

    private Rect rect;

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (v == null) return true;
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
            return true;
        case MotionEvent.ACTION_UP:
            if (rect != null
                    && !rect.contains(v.getLeft() + (int) event.getX(),
                        v.getTop() + (int) event.getY())) {
                // The motion event was outside of the view, handle this as a non-click event

                return true;
            }
            // The view was clicked.
            // TODO: do stuff
            return true;
        default:
            return true;
        }
    }
};
Ocular answered 19/1, 2015 at 9:4 Comment(0)
R
0

If drag to outside of view, 'ACTION_CANCEL' event call. So need disallow parent view to intercept touch event:

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            (parent as? ViewGroup?)?.requestDisallowInterceptTouchEvent(true)
        }
        MotionEvent.ACTION_UP -> {
            (parent as? ViewGroup?)?.requestDisallowInterceptTouchEvent(false)
        }
        else -> {
        }
    }
    return true
}

and then you can check touch point is outside of your view or not!

Robb answered 30/3, 2022 at 19:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.