Android: using onTouchEvent with a custom view in a custom viewgroup - android

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

Related

Android handle swipe on parent view and click on child

I am following the google developer documentation:
https://developer.android.com/training/gestures/viewgroup.html
I am doing the exact same case as their example code, where I have a swipe gesture on a parent, but if I am not swiping, then I will handle the click on the child as opposed to intercepting on the parent.
However the current flow is like this:
Action.DOWN - > Hits Parent's onInterceptTouchEvent I return false since I am not waiting for this case
Action.DOWN - > Hits Child's onTouch Handler, I return false since I still want to possibly use the parents code
Action.DOWN -> Hits Parents' onTouch Handler, I return true because i want to get the rest of the notifications for the gesture and then pass on to the child when appropriate. If I return false at this point, then the event will die and I wont get Action.UP
At this point in the gesture I am strictly relegated to the Parent's onTouchListener and feel pigeonholed there since there are conditions where I would like to go back down to the child's onTouch handler. Particularly confounding are the following stipulations in the Android documentation:
The down event will be handled either by a child of this view group, or given to your own onTouchEvent() method to handle; this means you should implement onTouchEvent() to return true, so you will continue to see the rest of the gesture (instead of looking for a parent view to handle it). Also, by returning true from onTouchEvent(), you will not receive any following events in onInterceptTouchEvent() and all touch processing must happen in onTouchEvent() like normal.
For as long as you return false from this function, each following event (up to and including the final up) will be delivered first here and then to the target's onTouchEvent().
So my desire is to get at least up until the point of Action.MOVE within the parent's onInterceptTouchEvent so that I can determine whether or not this gesture is the childs click/touch or the parent's swipe. I can only seem to get to ACTION_UP in my parent's onTouchEvent since I return true at Action.DOWN which relegates my entirely, as the documentation says, to my parents onTouch method.
How can i keep the onInterceptTouchEvent spinning while also implmenting the onTouchEvent never return true until the moment I want the parent to do something in lieu of the child?
That is my logical idea, but when I do that, I have to make my parent's onTouch return false for the ActionDown which then kills the event from both onTouch and onInterceptTouchEvent paradigms.
Parent's on touch listner:
binding.linLayoutWrapper.setOnTouchListener(new View.OnTouchListener() {
int downX, moveX, upX;
int downY, moveY;
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
//binding.linLayoutWrapper.getParent().requestDisallowInterceptTouchEvent(true);
downX = (int) motionEvent.getX();
downY = (int) motionEvent.getY();
return false;
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
return false;
} else if (motionEvent.getAction() == MotionEvent.ACTION_MOVE) {
moveY = (int) motionEvent.getY();
moveX = (int) motionEvent.getX();
if (Math.abs(downX - moveX) > Math.abs(downY - moveY)){
upX = (int) motionEvent.getX();
if (upX - downX > 100) {
// swipe right
CalendarDay cal = binding.calendarView.getCurrentDate();
Calendar cal1 = Calendar.getInstance();
cal1.setTime(cal.getDate());
cal1.add(Calendar.WEEK_OF_YEAR, -1);
binding.calendarView.setCurrentDate(cal1.getTime());
binding.linLayoutWrapper.getParent().requestDisallowInterceptTouchEvent(true);
return true;
} else if (downX - upX > 100) {
CalendarDay cal = binding.calendarView.getCurrentDate();
Calendar cal1 = Calendar.getInstance();
cal1.setTime(cal.getDate());
cal1.add(Calendar.WEEK_OF_YEAR, 1);
binding.calendarView.setCurrentDate(cal1.getTime());
binding.linLayoutWrapper.getParent().requestDisallowInterceptTouchEvent(true);
return true;
}
} else {
binding.linLayoutWrapper.getParent().requestDisallowInterceptTouchEvent(false);
}
}
return false;
}
});
Parents onInterceptTouchListener:
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
Log.d("HELP", " in parent action up");
mIsScrolling = false;
return false;
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (mIsScrolling) {
return true;
}
final int xDiff = calculateDistanceX(ev);
if (xDiff > mTouchSlop) {
// Start scrolling!
mIsScrolling = true;
return true;
}
break;
}
case MotionEvent.ACTION_DOWN: {
xNot = ev.getX();
return false;
}
}
return false;
}
Finally my child's onTouch listener:
relativeLayout.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
Intent intent = new Intent(getContext(), EditAvailabilityActivity.class);
if (event != null) {
int nurseId = AccountManager.sharedManager().getCurrentAccount().getId();
Conflict conflict = new Conflict();
conflict.setNurseId(nurseId);
conflict.setId(event.getConflictId());
conflict.setEndDate(event.getParentEnd());
conflict.setStartDate(event.getParentStart());
conflict.setStartTime(event.getStartTime());
conflict.setEndTime(event.getEndTime());
conflict.setIsAllDay(event.getAllDay() == 1);
intent.putExtra(EditAvailabilityActivity.EXTRA_CONFLICT, Parcels.wrap(conflict));
}
intent.putExtra(EditAvailabilityActivity.EXTRA_MODE, true);
((Activity) getContext()).startActivityForResult(intent, EDIT_AVAILABILITY_REQUEST_CODE);
return true;
}
return false;
}
});
UPDATE:
If I return true from the child's onTouchListener then it goes to the parent's onInterceptTouchEvent. I am not sure how that jives with what android says when they say " Also, by returning true from onTouchEvent(), you will not receive any following events in onInterceptTouchEvent() and all touch processing must happen in onTouchEvent() like normal." It seems like a flagrant contradiction, but maybe they mean "when you return true from the PARENT's onTouchListener" IS that accurate?
When I change my childs onTouchListener to this:
relativeLayout.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
Intent intent = new Intent(getContext(), EditAvailabilityActivity.class);
if (event != null) {
int nurseId = AccountManager.sharedManager().getCurrentAccount().getId();
Conflict conflict = new Conflict();
conflict.setNurseId(nurseId);
conflict.setId(event.getConflictId());
conflict.setEndDate(event.getParentEnd());
conflict.setStartDate(event.getParentStart());
conflict.setStartTime(event.getStartTime());
conflict.setEndTime(event.getEndTime());
conflict.setIsAllDay(event.getAllDay() == 1);
intent.putExtra(EditAvailabilityActivity.EXTRA_CONFLICT, Parcels.wrap(conflict));
}
intent.putExtra(EditAvailabilityActivity.EXTRA_MODE, true);
((Activity) getContext()).startActivityForResult(intent, EDIT_AVAILABILITY_REQUEST_CODE);
return true;
}
return true; //this makes it behave strangely
}
});
It effectively handles the click but it seems to do so in a weird way. It violates the android documentation by going back to the parent's oninterceptTouchEvent all the way through Action_UP. This seemed promising because if i could get that far along the gesture and still get the right outcome from the target's onTouch method then I would get what i was looking for. Sadly in this case, when I try to do a swipe gesture I only get the following rapartee between parent and child:
Action.DOWN - > Parent's onInterceptTouchEvent
Action.Down -> Hits Child's OnTouchListener which is returning "true"
Action.Cancel -> Hits Parent's onInterceptTouchEvent
This is the expected flow, so it was wishful thinking that it work but it was not in line with the earlier contradiction I noticed.

Android MotionEvent : Transfer between views

I have two custom viewgroups that implements onTouchListener for motionevent.
I am using a framelayout to show them both.
Second viewgroup is smaller in size than the first one.
So the first one is in background and second one is foreground.
I want to drag item for second/smaller viewgroup to the background viewgroup.
Problem : When dragging the item to the background viewgroup (i.e mAwesomePager), I want ACTION_UP to be triggered on the second viewgroup (smaller one) and ACTION_MOVE to be triggered on the first viewgroup, so basically the touchEvent is transferred from smaller viewgroup to the larger one in the background and the MotionEvent continues without the user has to take up the finger from the screen and then put it back again.
Here is some of the useful code :
#Override
public boolean onTouchEvent(MotionEvent ev) {
if (mRectOut == true) {
ev.setAction(MotionEvent.ACTION_UP);
mFrame.bringChildToFront(mAwesomePager);
//mAwesomePager.setClickable(true);
// Obtain MotionEvent object
long downTime = SystemClock.uptimeMillis();
long eventTime = SystemClock.uptimeMillis() + 100;
// List of meta states found here: developer.android.com/reference/android/view/KeyEvent.html#getMetaState()
int metaState = 0;
MotionEvent motionEvent = MotionEvent.obtain(
downTime,
eventTime,
MotionEvent.ACTION_DOWN,
ev.getX(),
ev.getY(),
metaState
);
mAwesomePager.getChildAt(mAwesomePager.mLastDragged).dispatchTouchEvent(motionEvent);
}
//some more code here
}
I am trying to simulate touch off in the foreground view by this ev.setAction(MotionEvent.ACTION_UP); , now I want the background view to take control over the touch while the touch is holding the dragged image.
Changing this
mAwesomePager.getChildAt(mAwesomePager.mLastDragged).dispatchTouchEvent(motionEvent);
to this
return mAwesomePager.dispatchTouchEvent(motionEvent);
eventually did the trick.
Taking a look at the documentation of the onTouchEvent() method, more specifically its return parameter:
Returns
True if the event was handled, false otherwise.
If you want your events to be passed to you background view, you need to make sure that when a ACTION_UP happens, the method returns false so Android knows the motion event was not consumed yet and should be relayed to the next view in the hierarchy.
Here's a basic snippet of what to do:
public boolean onTouchEvent(MotionEvent ev) {
if (event.getAction() == MotionEvent.ACTION_UP) {
// handle ACTION_UP with the foreground view
return false; // event will be sent to the background view
} else {
// handle ACTION_MOVE with the foreground view
return true; // event will stop here
}
}
Keep in mind that your background view will also need to implement the touch listener on its turn for this to happen.

How to vary between child and parent view group touch events

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;
}

How can I know the view is in touch state in Android

How can I know the view is in touch state.
If more than one touch points on one view,How can I catch the event of the last up touch point.
Please help?
You can override onTouchEvent() on your View. ACTION_DOWN will be given when the first "pointer" is placed. From then on, you will get ACTION_POINTER_DOWN or ACTION_POINTER_UP as subsequent fingers are pressed down and then released. Then, when the last pointer/finger is released, your View will get ACTION_UP. This is spelled out clearly in the MotionEvent docs.
Something like this might be what you're looking for, just subclass whatever View you are working with.
#Override
public boolean onTouchEvent(MotionEvent event)
{
if(event.getAction() == MotionEvent.ACTION_DOWN)
isTouching = true;
else if(event.getAction() == MotionEvent.ACTION_UP)
isTouching = false;
return super.onTouchEvent(event);
}

Overriding onTouchEvent competing with ScrollView

From a simplistic overview I have a custom View that contains some bitmaps the user can drag around and resize.
The way I do this is fairly standard as in I override onTouchEvent in my CustomView and check if the user is touching within an image, etc.
My problem comes when I want to place this CustomView in a ScrollView. This works, but the ScrollView and the CustomView seem to compete for MotionEvents, i.e. when I try to drag an image it either moves sluggishly or the view scrolls.
I'm thinking I may have to extend a ScrollView so I can override onInterceptTouchEvent and let it know if the user is within the bounds of an image not to try and scroll. But then because the ScrollView is higher up in the hierarchy how would I get access to the CustomView's current state?
Is there a better way?
Normally Android uses a long press to begin a drag in cases like these since it helps disambiguate when the user intends to drag an item vs. scroll the item's container. But if you have an unambiguous signal when the user begins dragging an item, try getParent().requestDisallowInterceptTouchEvent(true) from the custom view when you know the user is beginning a drag. (Docs for this method here.) This will prevent the ScrollView from intercepting touch events until the end of the current gesture.
None of the solutions found worked "out of the box" for me, probably because my custom view extends View, not ViewGroup, and thus I can't implement onInterceptTouchEvent.
Also calling getParent().requestDisallowInterceptTouchEvent(true) was throwing NPE, or doing nothing at all.
Finally this is how I solved the problem:
Inside your custom onTouchEvent call requestDisallow... when your view will take care of the event. For example:
#Override
public boolean onTouchEvent(MotionEvent event) {
Point pt = new Point( (int)event.getX(), (int)event.getY() );
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (/*this is an interesting event my View will handle*/) {
// here is the fix! now without NPE
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
clicked_on_image = true;
}
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (clicked_on_image) {
//do stuff, drag the image or whatever
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
clicked_on_image = false;
}
return true;
}
Now my custom view works fine, handling some events and letting scrollView catch the ones we don't care about. Found the solution here: http://android-devblog.blogspot.com.es/2011/01/scrolling-inside-scrollview.html
Hope it helps.
There is an Android event called MotionEvent.ACTION_CANCEL (value = 3). All I do is override my custom control's onTouchEvent method and capture this value. If I detect this condition then I respond accordingly.
Here is some code:
#Override
public boolean onTouchEvent(MotionEvent event) {
if(isTouchable) {
int maskedAction = event.getActionMasked();
if (maskedAction == MotionEvent.ACTION_DOWN) {
this.setTextColor(resources.getColor(R.color.octane_orange));
initialClick = event.getX();
} else if (maskedAction == MotionEvent.ACTION_UP) {
this.setTextColor(defaultTextColor);
endingClick = event.getX();
checkIfSwipeOrClick(initialClick, endingClick, range);
} else if(maskedAction == MotionEvent.ACTION_CANCEL)
this.setTextColor(defaultTextColor);
}
return true;
}

Categories

Resources