17

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

Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
Dori
  • 18,283
  • 17
  • 74
  • 116

5 Answers5

9

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.

Daniele Segato
  • 12,314
  • 6
  • 62
  • 88
  • whats wrong with overriding `ViewGroup#dispatchTouchEvent`? – pskink Feb 12 '16 at 16:16
  • @pskink as far as I know the dispatch method is not meant to be overridden. It contains security stuff and things like that. Furthermore I want to know if a child view disallowed the interception and I want the listener to know it. I didn't dig into the code enough to know if it can be safe to override dispatchTouchEvent. I simply think this is the way to intercept events that have the less chance to mess up android touch flow. The listener shouldn't be able to break the children flow. – Daniele Segato Feb 12 '16 at 17:34
  • @pskink oh, my way of doing it allow the listener to actually intercept the touch events at some point, unless it has been disallowed. Using dispatch you don't have that ability. – Daniele Segato Feb 12 '16 at 17:35
  • OP asks `I am intrested in all MotionEvents` so this is the answer for his exact question – pskink Feb 12 '16 at 17:39
  • This worked for me. An important note, which probably seems obvious: the InterceptTouchFrameLayout should *contain* the Views that will also get the touch events. My initial attempt of having it drawn over them didn't get the job done. – Bartholomew Furrow Oct 21 '18 at 19:19
6

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!

Roger Alien
  • 3,040
  • 1
  • 36
  • 46
Dori
  • 18,283
  • 17
  • 74
  • 116
  • So would this work fro a system overlay, to pass touch events to say the OS/browser/other apps after you get all touch data and act upon it? As of now it's all or nothing for my overlays and it's killing me! Thanks :-D – While-E Jan 31 '12 at 18:42
  • 3
    No. For security reasons you cannot easily 'snoop' all touch outside of your app. Having said that it is possible using the SYSTEM_ALERT_WINDOW permission and creating a transparent overlay. I have never tried this approach buts its described in the 'Tapjacking' chapter (23) of 'The busy coders guide to Android dev 2.3' http://commonsware.com/AdvAndroid/ – Dori Feb 01 '12 at 10:40
  • 2
    Could you post some code on how you did this? Im not following how you split up the ordinary routing and got all the motionEvents to your custom viewGroup and to the view who is consuming the events. – Joakim Palmkvist Nov 14 '13 at 07:57
  • I know this is an old post, but it would be great if you could explain some more on how you solved the issue. Maybe show some code ? – Eyad Alama May 23 '14 at 04:26
  • 1
    Even if you pass the event on to another method, you still have to return true of false from onInterceptTouchEvent(). If you return true from ACTION_DOWN, other Views in the hierarchy will no longer receive events for this gesture; if you return false, then this View won't receive subsequent MotionEvents for this gesture. I'm probably overlooking something, but it seems to me that you could have gone with the approach of overriding dispatchTouchEvent() in the Activity (monitoring instead of intercepting), as described in the question, and then translating the coordinates as needed. – Mark McClelland Mar 27 '15 at 07:41
2

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.

gnat
  • 6,213
  • 108
  • 53
  • 73
Chris Sullivan
  • 576
  • 5
  • 10
  • It looks to me like this won't have the desired effect of passing all events along to other Views in the hierarchy, except in the case where you return false. – Mark McClelland Mar 27 '15 at 07:32
2

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.

-1

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.

Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
David Knight
  • 43
  • 1
  • 5