Reputation: 18403
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
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
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
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
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
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