I am developing an app, which contains several fragments just like the picture below:
Picture 1
The bottom view is MainActivity, and there are several fragments above MainActivity, which may contain buttons. I want to add a fragment on top in order to listen the OnTouchEvents (gestures), but ignore the onclick events and pass the onclick events to the fragments below.
Here are my questions,
How can I distinguish onTouchEvent and onclick events in my codes?
How can I pass the onclick events to the fragments below?
I saw some people suggested to use onInterceptTouchEvent(), but I am not sure if this applies to my case and I do not really understand how to use onInterceptTouchEvent().
Update:
I tried to override boolean dispatchTouchEvent(MotionEvent ev) to make the top fragment ignore the click event, however since the first touch event must be MotionEvent.ACTION_DOWN, it seems that there is no way for me to determine whether the touch event is click.
So, is there any other way to do this?
From the Documentation
The onInterceptTouchEvent() method is called whenever a touch event is detected on the surface of a ViewGroup, including on the surface of its children. If onInterceptTouchEvent() returns true, the MotionEvent is intercepted, meaning it will be not be passed on to the child, but rather to the onTouchEvent() method of the parent.
The onInterceptTouchEvent() method gives a parent the chance to see any touch event before its children do. If you return true from onInterceptTouchEvent(), the child view that was previously handling touch events receives an ACTION_CANCEL, and the events from that point forward are sent to the parent's onTouchEvent() method for the usual handling. onInterceptTouchEvent() can also return false and simply spy on events as they travel down the view hierarchy to their usual targets, which will handle the events with their own onTouchEvent().
So, you'll have to return true/false (in onInterceptTouchEvent method) according to your application flow logic. If you use OnTouchListener you can avoid using OnClickListener. See example in the documentation to understand it better.
This will give you the idea how onTouch and onClick are different.
You can use custom interfaces to pass data to lower fragment for manipulation
I have a custom linearlayout in which I have two gridviews.
I want to have multitouch on the complete linearlayout, ie, first touch can be in one gridview and second touch can be in another gridview and the linear layout should be able to get multitouch events for this (ACTION_DOWN and ACTION_POINTER_DOWN). I am able to get this by overriding onInterceptTouchEvent in my custom layout.
Now I also want the gridview to scroll independently if required. Although gridview is a scrollable view, it is not able to do so, because I have intercepted the 'down' event. I see that when I try to scroll two events are generated : 'ACTION_DOWN' and 'ACTION_MOVE'.
Since I need ACTION_DOWN for multitouch use case I want it to be sent to linearlayout as well.
Is there a possibility to send the ACTION_DOWN event to both the linearlayout as well as my child view which is gridView in this case?
Need help.
You don't have to intercept ACTION_DOWN just to monitor that it flowed through your container view. In other words, you can override onInterceptTouchEvent() to monitor the touch events before they are passed to the appropriate child view by the framework, but as long as you don't screw with the return value it won't actually steal subsequent touch events from the grid elements. For example:
public boolean onInterceptTouchEvent (MotionEvent ev)
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
//Touchdown!
}
//Don't modify this and touch interaction will happen normally
return super.onInterceptTouchEvent(ev);
}
This allows you to see the event without taking it away from the child element(s).
How do I block onTouchEvent from a view's siblings? I have a ViewGroup with a custom button in it. When the button receives an OnTouchEvent, I want to block further OnTouchEvents from going to the button's siblings. I do want to continue receiving OnTouchEvent in the button.
I think you should use a custom ViewGroup as well. In order to get what you want you should override the onInterceptTouchEvent() method, like stated here:
The onInterceptTouchEvent() method gives a parent the chance to see
any touch event before its children do. If you return true from
onInterceptTouchEvent(), the child view that was previously handling
touch events receives an ACTION_CANCEL, and the events from that point
forward are sent to the parent's onTouchEvent() method for the usual
handling. onInterceptTouchEvent() can also return false and simply spy
on events as they travel down the view hierarchy to their usual
targets, which will handle the events with their own onTouchEvent().
Basically, according to your own logic (button onTouchEvent, etc), you should instruct your ViewGroup.onInterceptTouchEvent() to return true if MotionEvent raw coords are not included in the visible rect of your button.
The problem is the the scrolling will intercept with touch event has set to the parent layout.
Can I keep the onTouch event with the scroll in ScrollView ?
This is a very tricky part. There is an overriden method from Activity which is: public boolean onTouchEvent(MotionEvent event)
This is the general method that interprets all the touch events from the whole screen. And you could say, "ok, I can implement this and I am good to go..". And here comes the difficult part on how android works.
As you know every View has its own onTouchEvent() method that you could implement in order to add some custom implementation. So which method will listen? The ScrollView or the Activity? It appears that these touch events go from the "inside" elements to the "outside" elements. I mean parent-child relations.
Another thing to take into account is that the onTouchEvent method returns a boolean. This boolean parameter determines whether the touch event should go one level up or it is handled by the current View. Meaning that if you have a CustomViewA that implements the onTouchEvent() and CustomViewB implementing its own touch event, and the A is a child in B then the touch event would go through A first and if it is not handled it would go to B.
So basically yes it could be done. It depends on what touch event you wanted to do.
So in our case, the ScrollView returns true when the touch events are a horizontal. The activity's touch event will be handled only if the ScrollView touch event is not handled by itself then you are fine. Otherwise you have to override and implement the on touch event of scroll view and in some cases you have to return false so as for the whole layout to implement it. Good luck with the last part. I started to implement a fling effect but came up with some difficulties so I have implemented a 2 finger move with scroll view in it and it works like a charm.
This is about a week of research and experimenting and it is an overview of what I came up with. if you find anything else please let me know. Hope it helped.
I'm not asking how to handle touch events, but what is going on behind the scenes? If there are several nested widgets, what order do they see the events in? Does the developer have any control over it? Ideally I would like a document on the subject.
Let's take a look at a visual example.
When a touch event occurs, first everyone is notified of the event, starting at the Activity and going all the way to the view on top. Then everyone is given a chance to handle the event, starting with the view on top (view having highest Z order in the touch region) and going all the way back to the Activity. So the Activity is the first to hear of it and the last to be given a chance to handle it.
If some ViewGroup wants to handle the touch event right away (and not give anyone else down the line a chance at it) then it can just return true in its onInterceptTouchEvent(). An Activity doesn't have onInterceptTouchEvent() but you can override dispatchTouchEvent() to do the same thing.
If a View (or a ViewGroup) has an OnTouchListener, then the touch event is handled by OnTouchListener.onTouch(). Otherwise it is handled by onTouchEvent(). If onTouchEvent() returns true for any touch event, then the handling stops there. No one else down the line gets a chance at it.
More detailed explanation
The above diagram makes things a little more simple than they actually are. For example, between the Activity and ViewGroup A (the root layout) there is also the Window and the DecorView. I left them out above because we generally don't have to interact with them. However, I will include them below. The description below follows a touch event through the source code. You can click a link to see the actual source code.
(Update: the source code has been updated so the line numbers are off now, but clicking the links will still get you to the right file. Just do a search for the method name.)
The Activity's dispatchTouchEvent() is notified of a touch event. The touch event is passed in as a MotionEvent, which contains the x,y coordinates, time, type of event, and other information.
The touch event is sent to the Window's superDispatchTouchEvent(). Window is an abstract class. The actual implementation is PhoneWindow.
The next in line to get the notification is DecorView's superDispatchTouchEvent(). DecorView is what handles the status bar, navigation bar, content area, etc. It is actually just a FrameLayout subclass, which is itself a subclass of ViewGroup.
The next one to get the notification (correct me if I'm wrong) is the content view of your activity. That is what you set as the root layout of your activity in xml when you create the layout in the Android Studio's Layout Editor. So whether you choose a RelativeLayout, a LinearLayout, or a ConstraintLayout, they are all subclasses of ViewGroup. And ViewGroup gets notified of the touch event in dispatchTouchEvent(). This is the ViewGroup A in my diagrams above.
The ViewGroup will notify any children it has of the touch event, including any ViewGroup children. This is ViewGroup B in my diagrams above.
Anywhere along the way, a ViewGroup can short-circuit the notification process by returning true for onInterceptTouchEvent().
Assuming no ViewGroup cut the notifications short, the natural end of the line for the notifications is when the View's dispatchTouchEvent() get's called.
Now it is time, to start handling the events. If there is an OnTouchListener, then it gets the first chance at handling the touch event with onTouch(). Otherwise, the View's onTouchEvent() gets to handle it.
Now all the ViewGroups recursively up the line get a chance to handle the touch event in the same way that View did. Although, I didn't indicate this in the diagram above, a ViewGroup is a View subclass, so everything I described about OnTouchListener.onTouch() and onTouchEvent() also applies to ViewGroups.
Finally, if no one else wanted it, the Activity also gets the last chance to handle the event with onTouchEvent().
FAQ
When would I ever need to override dispatchTouchEvent()?
Override it in the Activity if you want to catch a touch event before any of the views get a chance at it. For a ViewGroup (including the root view), then just override onInterceptTouchEvent() and onTouchEvent().
When would I ever need to override onInterceptTouchEvent()?
If you just want to spy of the touch notifications that are coming in, you can do it here and return false.
However, the main purpose of overriding this method is to let the ViewGroup handle a certain type of touch event while letting the child handle another type. For example, a ScrollView does this to handle scrolling while letting its child handle something like a Button click. Conversely, if the child view doesn't want to let its parent steal its touch event, it can call requestDisallowTouchIntercept().
What are the touch event types?
The main ones are
ACTION_DOWN - This is the start of a touch event. You should always return true for the ACTION_DOWN event in onTouchEvent if you want to handle a touch event. Otherwise, you won't get any more events delivered to you.
ACTION_MOVE - This event is continuously fired as you move your finger across the screen.
ACTION_UP - This is the last event of a touch event.
A runner up is ACTION_CANCEL. This gets called if a ViewGroup up the tree decides to intercept the touch event.
You can view the other kinds of MotionEvents here. Since Android is multi-touch, events are also fired when other fingers ("pointers") touch the screen.
Further study
Android onTouchEvent Part 1, Part 2, and Part 3 (YouTube video - good summary of some of the links below)
Mastering the Android Touch System (thorough video by Google developer)
Android UI Internal : Pipeline of View's Touch Event Handling
Managing Touch Events in a ViewGroup (Android docs)
Input Events (Android docs)
Gestures and Touch Events
From Activity viewpoint:
Touch events are delivered first to Activity.dispatchTouchEvent. It's where you may catch them first.
Here they get dispatched to Window, where they traverse View hierarchy, in such order that Widgets that are drawn last (on top of other widgets) have chance to process touch in View.onTouchEvent first. If some View returns true in onTouchEvent, then traversal stops and other Views don't receive touch event.
Finally, if no View consumes touch, it's delivered to Activity.onTouchEvent.
That's all your control. And it's logical that what you see drawn on top of something else, has chance to process touch event before something drawn below it.
Android Touch event
I have prepared a high level diagram that should illustrate a simple flow.
dispatchTouchEvent() - Activity, ViewGroup, View
onInterceptTouchEvent() - ViewGroup
onTouch() - ViewGroup, View. Using setOnTouchListener()
onTouchEvent() - Activity, ViewGroup, View
[iOS onTouch]
following Suragch's answer,
pseudocode:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
ref:Android开发艺术探索