Dori
Dori

Reputation: 18403

Android - Intercept and pass on all touch events

I have an overlay ViewGroup that is the size of the screen which I want to use to show an effect when the user interacts with the app, but still passes the onTouch event to any underlying views.

I am intrested in all MotionEvents (not just DOWN), so onInterceptTouchEvent() does not apply here as if i return true my overlay will consume all events, and if false will only receive the DOWN events (the same applys to onTouch).

I thought I could override the Activitys dispatchTouchEvent(MotionEvent ev) and call a custom touch event in my overlay, but this has the effect of not translating the input coords depending on the position of my view (for example all events will pass appear to be happening 20 or so px below the actual touch as the system bar is not taken into account).

Upvotes: 17

Views: 43561

Answers (5)

Daniele Segato
Daniele Segato

Reputation: 12899

The only proper way to build such an interceptor is using a custom ViewGroup layout.

But implementing ViewGroup.onInterceptTouchEvent() is just not enough to catch every touch events because child views can call ViewParent.requestDisallowInterceptTouchEvent(): if the child does that your view will stop receiving calls to interceptTouchEvent (see here for an example on when a child view can do that).

Here's a class I wrote (Java, long ago...) and use when I need something like this, it's custom FrameLayout that gives full control on touch events on a delegator

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.FrameLayout;

/**
 * A FrameLayout that allow setting a delegate for intercept touch event
 */
public class InterceptTouchFrameLayout extends FrameLayout {
    private boolean mDisallowIntercept;

    public interface OnInterceptTouchEventListener {
        /**
         * If disallowIntercept is true the touch event can't be stealed and the return value is ignored.
         * @see android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
         */
        boolean onInterceptTouchEvent(InterceptTouchFrameLayout view, MotionEvent ev, boolean disallowIntercept);

        /**
         * @see android.view.View#onTouchEvent(android.view.MotionEvent)
         */
        boolean onTouchEvent(InterceptTouchFrameLayout view, MotionEvent event);
    }

    private static final class DummyInterceptTouchEventListener implements OnInterceptTouchEventListener {
        @Override
        public boolean onInterceptTouchEvent(InterceptTouchFrameLayout view, MotionEvent ev, boolean disallowIntercept) {
            return false;
        }
        @Override
        public boolean onTouchEvent(InterceptTouchFrameLayout view, MotionEvent event) {
            return false;
        }
    }

    private static final OnInterceptTouchEventListener DUMMY_LISTENER = new DummyInterceptTouchEventListener();

    private OnInterceptTouchEventListener mInterceptTouchEventListener = DUMMY_LISTENER;

    public InterceptTouchFrameLayout(Context context) {
        super(context);
    }

    public InterceptTouchFrameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public InterceptTouchFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public InterceptTouchFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyle) {
        super(context, attrs, defStyleAttr, defStyle);
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        getParent().requestDisallowInterceptTouchEvent(disallowIntercept);
        mDisallowIntercept = disallowIntercept;
    }

    public void setOnInterceptTouchEventListener(OnInterceptTouchEventListener interceptTouchEventListener) {
        mInterceptTouchEventListener = interceptTouchEventListener != null ? interceptTouchEventListener : DUMMY_LISTENER;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean stealTouchEvent = mInterceptTouchEventListener.onInterceptTouchEvent(this, ev, mDisallowIntercept);
        return stealTouchEvent && !mDisallowIntercept || super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean handled = mInterceptTouchEventListener.onTouchEvent(this, event);
        return handled || super.onTouchEvent(event);
    }
}

This class provide a setInterceptTouchEventListener() to set your custom interceptor.

When it is required to disallow intercepting touch event it cheats: pass the disallow to the parent view, like it's supposed to, but keeps intercepting them. However, it does not let the listener intercept events anymore, so if the listener return true on intercept touch event that will be ignored (you can choose to throw an IllegalStateException instead if you prefer.

This way you can transparently receive every touch event that pass through your ViewGroup without disrupting the children views behavior.

Upvotes: 9

David Knight
David Knight

Reputation: 43

I tried all of the solutions in the above answers however still had issues receiving all touch events while still allowing children views to consume the events. In my scenario I would like to listen in on events that a child views would consume. I could not receive follow up events after a overlapping child view started consuming events.

I finally found a solution that works. Using the RxBinding library you can observe all touch events including events that are consumed by overlapping children views.

Kotlin Code snippet

RxView.touches(view)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(

        object  : Observer<MotionEvent> {
            override fun onCompleted() {
            }

            override fun onNext(t: MotionEvent?) {
                Log.d("motion event onNext $t")
            }

            override fun onError(e: Throwable?) {
            }
        }
    )

See here and here for more details.

Upvotes: -1

Tathagata dey
Tathagata dey

Reputation: 21

As touch flows starting from root view to child views and after reaching the bottom most child it(touch) propagate back to root view while traversing all the OnTouchListener, if the actions consumable by child views return true and false for rest, then for the actions that you want to intercept can be written under OnTouchListner of those parents.

Like if root view A has two child X and Y and you want to intercept swipe touch in A and want to propagate rest of touch to child views(X and Y) then override OnTouchListner of X and Y by returning false for swipe and true for rest. And override OnTouchListner of A with swipe returning true and rest returning false.

Upvotes: 2

Dori
Dori

Reputation: 18403

I solved this with a non perfect solution but works in this situation.

I created another ViewGroup which contains a clickable child which has width and height set to fill_parent (this was my requirement before adding the effects) and have overridden onIntercept...(). In onIntercept... I pass the event on to my overlay view to a custom onDoubleTouch(MotionEvent) method and do the relevant processing there, without interrupting the platforms routing of input events and letting the underlying viewgroup act as normal.

Easy!

Upvotes: 6

Chris Sullivan
Chris Sullivan

Reputation: 586

Here's what I did, building on Dori's answer I used onInterceptTouch . . .

float xPre = 0;

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    if(Math.abs(xPre - event.getX()) < 40){
        return false;
    }
    if(event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_UP  || event.getAction() == MotionEvent.ACTION_MOVE){
        xPre = event.getX();
    }
    return true;
}

Basically, when any important movement is made, remember where it happened, via xPre; then if that point is close enough to the next point to be say, an onClick/onTouch of the child view return false and let onTouch do it's default thing.

Upvotes: 2

Related Questions