androidandroid-gestureandroid-touch-event

onTouch: detect ACTION_UP without blocking other touch events


I have a custom view which acts as a button. I am drawing all the canvas myself. Now I'm making an outline when ACTION_DOWN and remove it after ACTION_UP or ACTION_CANCEL.

@Override
public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.e("test", "ACTION_DOWN");
            break;
        case MotionEvent.ACTION_UP:
            Log.e("test", "ACTION_UP");
            break;
        case MotionEvent.ACTION_CANCEL:
            Log.e("test", "ACTION_CANCEL");
            break;
    }

    return true;
}

This can work for me, except now it is blocking another gesture behind this view which is detecting ACTION_MOVE (scroll left).

If I return false, then it is working fine but now ACTION_UP is not called.

I want to call ACTION_UP if finger is lifted, but pass events down otherwise.


Solution

  • Have you tried overriding dispatchTouchEvent?

    https://developer.android.com/reference/android/view/View.html#dispatchTouchEvent(android.view.MotionEvent)

    UPDATE:

    So touch events are a bit of a beast. The rundown of it is this...

    1. They bubble up at first from your root container in your Activity. This is done by calling dispatchTouchEvent and then onInterceptTouchEvent assuming intercepting wasn't blocked by a child view.
    2. If no view intercepts the event, it will bubble to the leaf node (such as a button) where onTouch is called. If the node doesn't handle it (returns true) its parent gets a chance and so on.

    This means that you can use dispatchTouchEvent or onInterceptTouchEvent to spy on touch events without changing the behavior. Unless you're actually going to intercept the event I suggest using dispatchTouchEvent as it's guaranteed to run whereas intercepting may be blocked (example: DrawerLayout will intercept touch events near the edge in order to open the drawer).

    So the final result is:

    public class MyView extends Button {
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    Log.e("test", "ACTION_DOWN");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e("test", "ACTION_UP");
                    break;
                case MotionEvent.ACTION_CANCEL:
                    Log.e("test", "ACTION_CANCEL");
                    break;
            }
            return super.dispatchTouchEvent(event);
        }
    }
    

    UPDATE:

    Sorry, so I've been under the impression for some reason (mostly my poor reading) that we were dealing with the parent. Here's what I would do...

    Go ahead and implement onTouch and return true to consume all the events. This means that any touch events that start on your view will be eaten up. What we'll do then is translate the point to the parent's coordinate space and manually pass the touch event up, it'll look like this inside your custom view...

    private boolean passingTouchEventToParent = true;
    final private Rect hitRect = Rect();
    @Override
    public boolean onTouch(MotionEvent event) {
        // Handle your custom logic here
    
        final ViewParent viewParent = getParent();
        if (passingTouchEventToParent &&
                viewParent != null &&
                viewParent instanceof View) {
            // Gets this view's hit rectangle in the parent's space
            getHitRect(hitRect);
            event.offsetLocation((float) hitRect.left, (float) hitRect.top);
            passingTouchEventToParent = viewParent.onTouchEvent(event);
        }
        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
            // Motion event finished, reset passingTouchEventToParent
            passingTouchEventToParent = true;
        }
        return true;
    }