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
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.
The only way to build such an interceptor is using a custom ViewGroup layout.
But implementing ViewGroup.onInterceptTouchEvent() is just not enough in any occasion because child views can call ViewParent.requestDisallowInterceptTouchEvent() and if the child does that your view will stop receiving calls to interceptTouchEvent (see here for an example on when I child you can do that).
Here's a class I wrote and use when I need something like this, it's custom FrameLayout that gives full control on touch events on any 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 cheat, it pass this to the parent view 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.
This way you can transparently receive every touch event that pass through your ViewGroup without disrupting the children views behavior.
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.
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.
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!