I decided to post this question and answer in response to this comment to this question:
How to handle click in the child Views, and touch in the parent ViewGroups?
I will paste the comment here:
Suppose I want to override the touch events only for handling some of
the children, what can I do inside this function to have it working ?
I mean, for some children it would work as usual, and for some, the
parent-view will decide if they will get the touch events or not.
So the question is this: How do I prevent the parent onTouchEvent() from overriding some child elements' onTouchEvent(), while having it override those of other children?
The onTouchEvents() for nested view groups can be managed by the boolean onInterceptTouchEvent.
The default value for the OnInterceptTouchEvent is false.
The parent's onTouchEvent is received before the child's. If the OnInterceptTouchEvent returns false, it sends the motion event down the chain to the child's OnTouchEvent handler. If it returns true the parent's will handle the touch event.
However there may be instances when we want some child elements to manage OnTouchEvents and some to be managed by the parent view (or possibly the parent of the parent).
This can be managed in more than one way.
One way a child element can be protected from the parent's OnInterceptTouchEvent is by implementing the requestDisallowInterceptTouchEvent.
public void requestDisallowInterceptTouchEvent (boolean
disallowIntercept)
This prevents any of the parent views from managing the OnTouchEvent for this element, if the element has event handlers enabled.
If the OnInterceptTouchEvent is false, the child element's OnTouchEvent will be evaluated. If you have a methods within the child elements handling the various touch events, any related event handlers that are disabled will return the OnTouchEvent to the parent.
This answer:
https://stackoverflow.com/a/13540006/3956566 gives a good visualisation of how the propagation of touch events passes through:
parent -> child|parent -> child|parent -> child views.
Another way is returning varying values from the OnInterceptTouchEvent for the parent.
This example taken from Managing Touch Events in a ViewGroup and demonstrates how to intercept the child's OnTouchEvent when the user is scrolling.
4a.
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onTouchEvent will be called and we do the actual
* scrolling there.
*/
final int action = MotionEventCompat.getActionMasked(ev);
// Always handle the case of the touch gesture being complete.
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// Release the scroll.
mIsScrolling = false;
return false; // Do not intercept touch event, let the child handle it
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (mIsScrolling) {
// We're currently scrolling, so yes, intercept the
// touch event!
return true;
}
// If the user has dragged her finger horizontally more than
// the touch slop, start the scroll
// left as an exercise for the reader
final int xDiff = calculateDistanceX(ev);
// Touch slop should be calculated using ViewConfiguration
// constants.
if (xDiff > mTouchSlop) {
// Start scrolling!
mIsScrolling = true;
return true;
}
break;
}
...
}
// In general, we don't want to intercept touch events. They should be
// handled by the child view.
return false;
}
Edit: To answer comments.
This is some code from the same link showing how to create the parameters of the rectangle around your element:
4b.
// The hit rectangle for the ImageButton
myButton.getHitRect(delegateArea);
// Extend the touch area of the ImageButton beyond its bounds
// on the right and bottom.
delegateArea.right += 100;
delegateArea.bottom += 100;
// Instantiate a TouchDelegate.
// "delegateArea" is the bounds in local coordinates of
// the containing view to be mapped to the delegate view.
// "myButton" is the child view that should receive motion
// events.
TouchDelegate touchDelegate = new TouchDelegate(delegateArea, myButton);
// Sets the TouchDelegate on the parent view, such that touches
// within the touch delegate bounds are routed to the child.
if (View.class.isInstance(myButton.getParent())) {
((View) myButton.getParent()).setTouchDelegate(touchDelegate);
}
Lets revamp the issue.
You happen to have a ViewGroup with a bunch of children. You want to intercept the touch event for everything withing this ViewGroup with a minor exception of some children.
I have been looking for an answer for the same question for quite a while. Did not manage to find anything reasonable and thus came up on my own with the following solution.
The following code snippet provides an overview of the ViewGroup's relevant code that intercepts all touches with the exception of the ones coming from views that happen to have a special tag set (You should set it elsewhere in your code).
private static int NO_INTERCEPTION;
private boolean isWithinBounds(View view, MotionEvent ev) {
int xPoint = Math.round(ev.getRawX());
int yPoint = Math.round(ev.getRawY());
int[] l = new int[2];
view.getLocationOnScreen(l);
int x = l[0];
int y = l[1];
int w = view.getWidth();
int h = view.getHeight();
return !(xPoint < x || xPoint > x + w || yPoint < y || yPoint > y + h);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
for (int i=0; i<floatingMenuItems.getChildCount(); i++){
View child = floatingMenuItems.getChildAt(i);
if (child == null || child.getTag(NO_INTERCEPTION) == null) {
continue;
}
if(isWithinBounds(child, ev)){
return false;
}
}
return true;
}
Related
Disclaimer: I have already tried following similar threads (namely How can I use OnClickListener in parent View and onTouchEvent in Child View? and How can i get both OnClick and OnTouch Listeners), yet my code still fails to work.
I have a (parent) activity which contains a (child) view. Activity has all the network code, while view contains a bitmap displaying game state. View executes the game moves and draws them, while activity sends and receives the moves.
As such, I need to listen for player's actions on the bitmap, handle them there and immediately after it is handled in the view, I need to send the made move to the server. So the best way I've found to handle all that so far is by using onClickListener along with onTouchListener. But I've spent 4 hours and can't get this to work, so any help would be appreciated.
My parent onClick:
gameView.setOnClickListener(new View.OnClickListener(){
#Override
public void onClick(View v){
//gameView.performClick();
sendMove(theGame.getLastMove());
}
});
My child view:
#Override
public boolean onTouchEvent(MotionEvent event)
{
if(canIMove) {
float x = event.getX();
float y = event.getY();
int recWidth = this.getWidth() / 7;
int currentWidth = 0;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (x > currentWidth && x < currentWidth + recWidth)
{
//Bitmap touched
theGame.Move();
ReloadGameBoard();
}
return true;
}
}
return false;
}
It matters not whether I try to performClick() in onClick or in onTouch, it doesn't also matter whether I return true or false in onTouch, it doesn't matter whether I put it in ACTION_DOWN nor ACTION_UP, the result is always the same: child View onTouch gets executed and parent onClick does not.
And please, if you decide to downvote this, at least tell me why.
How can I find the view causing a MotionEvent ACTION_CANCEL? I have a view "A" which is receiving ACTION_CANCEL and I don't want that to happen. Somewhere, a view "B" is "consuming" the MotionEvent. I'm hoping there is a way to find out who is "B" so I can address this misfunctionality.
I've tried looking through my code for various OnTouchEvent() and OnInterceptTouchEvent() handlers but haven't yet found a culprit.
I've also put a break point at the problematic ACTION_CANCEL but am not able to recognize anything in the MotionEvent that might represent "B".
If a parent is intercepting the motion event, the only way to prevent this is to prevent the parent from
intercepting that event. This can be managed well in two ways.
Without seeing specific code and wanting a generalised solution I would suggest the following.
I would suggest managing your touch events for the parents and the child by managing the
requestDisallowInterceptTouchEvent(boolean) and
onInterceptTouchEvent(android.view.MotionEvent) event handlers of every view/viewgroup within the affected views A, B C.
By disallowing parent intercepts in the child, this assists you in catching parent intercepts you
haven't accounted for, and also to customise and vary the child elements from within one parent.
This must be managed from your highest parent of your view/viewGroup and managed through all parent and
child relationships.
Checking for listviews, any element that has a inbuilt touch events.
android.com/training/gestures/viewgroup
In terms of finding the offending view that is intercepting the event, that cannot be answered except by the logic of:
Go through each parent to child/parent to child view. Methodically check the ontouch handling within each view/view group
as shown in my diagram.
There is some more detail in these answers here:
https://stackoverflow.com/a/30966413/3956566
https://stackoverflow.com/a/6384443/3956566
I'm sure you understand this, but to me, it's the simplest solution.
Beyond this, we'd need to look at your code to troubleshoot it.
If I got your question correct, you are receiving ACTION_CANCEL probably in the parent and you need to find that view. Given event X and Y, you can find view that contains these coordinates at the first moment ACTION_CANCEL occurred. Try calling this method either with top parent (android.R.id.content) or the ViewGroup you are dealing with.
private View findViewAt(View contentView, int eventX, int eventY) {
ArrayList<View> unvisited = new ArrayList<View>();
unvisited.add(contentView);
while (!unvisited.isEmpty()) {
View child = unvisited.remove(0);
if(isViewContains(child, eventX, eventY) {
Log.i(TAG, "view found! ");
unvisited.clear();
return child;
}
if (!(child instanceof ViewGroup)){
continue;
}
ViewGroup group = (ViewGroup) child;
final int childCount = group.getChildCount();
for (int i=0; i< childCount; i++){
unvisited.add(group.getChildAt(i));
}
}
return null;
}
private boolean isViewContains(View view, int eventX, int eventY) {
int[] location = new int[2];
view.getLocationOnScreen(location);
int x = location[0];
int y = location[1];
int width = view.getWidth();
int height = view.getHeight();
return eventX < x || eventX > x + width || eventY < y || eventY > y + height;
}
On my experience , a case that makes A view receive a ACTION_CANCEL event is after A is touched by users and they drags their finger out of area of A, If you face this case,Add a method to check the location on dispatchTouchEvent() can help.
i'm using a SemiClosedSlidingDrawer (http://pastebin.com/FtVyrcEb) and i've added on content part some buttons on the top of slider which are always visibles.
The problems is that they are clickable (or click event is dispatched) only when slider is fully opened... When slider is "semi-opened" click event not seems dispached to button... I have inspected with debugger into onInterceptTouchEvent() and in both cases (opened/semi-collapsed) the following code
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (mLocked) {
return false;
}
final int action = event.getAction();
float x = event.getX();
float y = event.getY();
final Rect frame = mFrame;
final View handle = mHandle;
handle.getHitRect(frame);
//FOLLOWING THE CRITICAL CODE
if (!mTracking && !frame.contains((int) x, (int) y)) {
return false;
}
return false but only when slider is opened event was dispached...
It checks if a (x,y) relative to the click are contained in a rectangle created starting from the HandleButton view of sliding drawer...
final Rect frame = mFrame;
final View handle = mHandle;
handle.getHitRect(frame);
and this is obviously false because i'm clicking on a button contained inside the content part of slidingdrawer and that's ok...
As i said above the problem is that in semi-collapsed state, buttons contained in content part are not receiving the event...
Have you any idea how can i solve this issue?
Can be some state of slidingdrawer that avoid to click childs when collapsed?
Thanks in advance...
Right, I think I've figured out a way to do this.
First you need to modify onInterceptTouchEvent() to return true whenever the user presses the visible content during the semi-opened state. So, for instance, if your SemiClosedSlidingDrawer view is located at the very bottom of the screen, you can use a simple detection algorithm, like this:
public boolean onInterceptTouchEvent(MotionEvent event) {
...
handle.getHitRect(frame);
// NEW: Check if the user pressed on the "semi-open" content (below the handle):
if(!mTracking && (y >= frame.bottom) && action == MotionEvent.ACTION_DOWN) {
return true;
}
if (!mTracking && !frame.contains((int) x, (int) y)) {
...
}
Now the touch events during the user's interaction with the semi-opened content will be dispatched to onTouchEvent(). Now we just need to intercept these events and "manually" redirect them to the right view (note that we also need to offset the coordinates for the child view):
public boolean onTouchEvent(MotionEvent event) {
...
if (mTracking) {
...
}
else
{
// NEW: Dispatch events to the "semi-open" view:
final Rect frame = mFrame;
final View handle = mHandle;
handle.getHitRect(frame);
float x = event.getX();
float y = event.getY() - frame.bottom;
MotionEvent newEvent = MotionEvent.obtain(event);
newEvent.setLocation(x, y);
return mContent.dispatchTouchEvent(newEvent);
}
return mTracking || mAnimating || super.onTouchEvent(event);
}
It's a bit of a messy implementation, but I think the basic concept is right. Let me know how it works for you!
today I got a problem with touch event handling on android custom views.In this case i have created parent view call weekview and chiled call weekdayview.i want implement touch event like singleTap,LongPress in child view only and when i swipe on parent or child i wanna scroll parent view.when i implement touch event in both view it dose not work.
can anyone help me on this.It's really helpful to me.
Thank you
class ChildView extends View {
public void setGestureDetector (GestureDetector g)
{
gesture = g;
}
#Override
public boolean onTouchEvent (....)
{
return gesture.onTouchEvent (....); // touch event will dispatch to gesture
}
}
class ParentView extends View implements GestureDetector.OnGestureListener {
gesture = new GestureDetector (this);
child = new ChildView (...);
child.setGestureDetector (gesture);
#Override
public boolean onTouchEvent (..)
{
// handle your parent touch event here
}
public boolean onDown (...)
{
return true;
}
public boolean fling (...)
{
// here to handle child view fling
}
}
This is peso-code (not real android java) to show you the concept to use GestureDetector, you can deal with all events from your child view in your parent View. As I tested on my android phone, onTouchEvent in ChildView didn't recognize ACTION_UP very well, so even you swipe your Child View, sometimes fling will not work (it depends on ACTION_UP).
So if you want to write more accurate swipe on your Child View, better write your owen Gesture Detect class, and in your ChildView, you can do this -
float oldX;
float distanceX;
public boolean onTouchEvent (MotionEvent event...)
{
if (event.getAction == MotionEvent.ACTION_DOWN) {
// handle down
oldX = event.getX ();
}
if (event.getAction == MotionEvent.ACTION_MOVE {
// handle move
distanceX = event.getX() - oldX; // more accurate
gesture.onSwipe (distanceX); // your own gesture class
}
}
Set your swipe velocity (to detect user's intention to swipe) and override the onTouchEvent() in your child view.
Here, call super.onTouchEvent() and return, which calls your parent view. Handle the events in the parent view.
I have a custom view which I call "Node" that is a child of a custom ViewGroup called "NodeGrid". The "NodeGrid" class more specifically extends RelativeLayout.
I have the following code snippet in my custom view class ("Node"):
private boolean isBeingDragged = false;
#Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN)
{
isBeingDragged = true;
}
else if (event.getAction() == MotionEvent.ACTION_UP)
{
isBeingDragged = false;
}
else if (event.getAction() == MotionEvent.ACTION_MOVE)
{
if (isBeingDragged)
{
float xPosition = event.getX();
float yPosition = event.getY();
//change the x and y position here
}
}
return false;
}
The problem:
After having set breakpoints in this code, it seems like onTouchEvent is getting called only for the MotionEvent.ACTION_DOWN case, but not for either of the other two cases ("action up" or "action move"). Does anyone know of anything off hand that could be causing this to happen?
Also (could be related):
Does it matter how the view is added to the ViewGroup? I noticed that in addition to "addView" there are other methods for adding children to a ViewGroup such as "addFocusables" and "addTouchables". Right now I am simply adding the child view to the ViewGroup using "addView".
From the SDK Documentation:
onTouch() - This returns a boolean to indicate whether your listener consumes this event. The important thing is that this event can have multiple actions that follow each other. So, if you return false when the down action event is received, you indicate that you have not consumed the event and are also not interested in subsequent actions from this event. Thus, you will not be called for any other actions within the event, such as a finger gesture, or the eventual up action event.
You need to return true when the ACTION_DOWN event is triggered to indicate that you are interested in the subsequent calls relating to that same event.
HTH