I'm using an on touch listener to display and hide some volume controls, on ACTION_DOWN the controls are displayed and on ACTION_UP they are hidden. I want to be able to touch the controls without lifting my finger, I tried using the ACTION_MOVE motion and was unable to get it to work as the event is never triggered. I thought about drag event but I am unsure if it would be appropriate for this.
public boolean onTouch(View v, MotionEvent e)
{
if(v == audioControls)
{
if(e.getAction() == MotionEvent.ACTION_DOWN)
showVolumeControls();
else if(e.getAction() == MotionEvent.ACTION_UP)
hideVolumeControls();
}
else if(e.getAction() == MotionEvent.ACTION_MOVE)
{
if(v == mute)
//Do stuff with this volume control
}
return true;
}
#Demand answer, read my comment - here is the code:
public boolean onTouch(View v, MotionEvent e)
{
if(v == mute && e.getAction() == MotionEvent.ACTION_MOVE)
{
Toast.makeText(getApplicationContext(), "Muted.", Toast.LENGTH_SHORT).show();
hideVolumeControls();
return true;
}
else
return false;
}
So, you need to uderstand how android touch events work. If you touch down on View1, set onTouchListener for that view and return true for that event - other view will never get motion events from same chain.
For you it's mean that if you touch down on "audioControls" then no other views can catch motion events until you release your finger.
You can return false in your onTouch method. In this case parentView for audioControls will also catch all motionEvents. But views, which is not parent for audioControls in the view hierarchy will not catch motionEvent.
You need to catch all motion events in the container for your views and dispatch them for your self. This is the only way to catch motionEvents from one chain in defferent views.
UPDATE:
I will try to explain a little bit more.
Imagine you have layout like this:
<LinearLayout id="#+id/container">
<View id="#+id/view1"/>
<View id="#+id/view2"/>
</LinearLayout>
And you want to touch down on view1 and move your finger to view2. Android touch event flow can't do this. Only one view can catch whole event chain.
So you need to add onTouchListener to your container and do something like this.
public boolean onTouch(MotionEvent event) {
float x = event.getX();
float y = event.getY();
for (int i = 0; i<container.getChildCount(); i++) {
View child = container.getChildAt(i);
if (x>child.getLeft() && x < child.getRight() && y < child.getBottom() && y > child.getTop()) {
/*do whatever you want with this view. Child will be view1 or view2, depends on coords;*/
break;
}
}
}
Please note, I wrote this code right there and could make some mistake. I've tried to show the idea.
Related
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.
I have two view pager, one is nested in the other. To get the correct behaviour (swiping the inner view pager without changing the outer) I had to override the inner view pagers onTouchListener and put all my onTouch/onClick logic into it (got the idea from here).
Works all fine, but since I don't have a onClickListener anymore I lost my selector effect. When I put android:clickable="true" on the layout element I get my selector effect, but the view pagers behaviour is wrong again.
Is there any way to achieve the selector effect out of the onTouchListener?
innerPager.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (gestureDetector.onTouchEvent(event)) {
Log.d(DEBUG_LOG, "Single tap.");
return true;
} else if(event.getAction() == MotionEvent.ACTION_DOWN && v instanceof ViewGroup) {
((ViewGroup) v).requestDisallowInterceptTouchEvent(true);
}
return false;
}
});
This solved it for me. Just added the following code to my OnTouchListener and replaced the card view with my inner view pagers current item:
// Since the host view actually supports clicks, you can return false
// from your touch listener and continue to receive events.
myTextView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent e) {
// Convert to card view coordinates. Assumes the host view is
// a direct child and the card view is not scrollable.
float x = e.getX() + v.getLeft();
float y = e.getY() + v.getTop();
// Simulate motion on the card view.
myCardView.drawableHotspotChanged(x, y);
// Simulate pressed state on the card view.
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
myCardView.setPressed(true);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
myCardView.setPressed(false);
break;
}
// Pass all events through to the host view.
return false;
}
});
I'm trying a wild idea here by putting a custom control within the items of a certain list view. The control is only "activated" if the user touches down on a certain trigger point and then they can "drag around."
My question is, what can I do in onTouchEvent(...) to prevent the listview from receiving the event and scrolling. Right now I can touch and get ahold of the control, but if I move my finger too much up or down the listview takes over and starts scrolling, then my view doesn't even receive a ACTION_UP event.
Here is my current onTouchEvent code:
if (e.getAction() == MotionEvent.ACTION_DOWN) {
Log.d("SwipeView", "onTouchEvent - ACTION_DOWN" + e.getX() + " " + e.getY());
int midX = (int)(this.getWidth() / 2);
int midY = (int)(this.getHeight() / 2);
if (Math.abs(e.getX() - midX) < 100 &&
Math.abs(e.getY() - midY) < 100) {
Log.d("SwipeView", "HEY");
setDragActive(true);
}
this.invalidate();
return true;
} else if (e.getAction() == MotionEvent.ACTION_MOVE) {
_current[0] = e.getX();
_current[1] = e.getY();
this.invalidate();
return true;
} else if (e.getAction() == MotionEvent.ACTION_UP) {
_current[0] = 0;
_current[1] = 0;
setDragActive(false);
this.invalidate();
return true;
}
I'm sure it has something to do with the event hierarchy in some fashion.
This might not be exactly what you're looking for, but it's possible to implement capture capabilities in your activity. add
private View capturedView;
public void setCapturedView(View view) { this.capturedView = view); }
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
return (this.capturedView != null) ?
this.capturedView.dispatchTouchEvent(event) :
super.dispatchTouchEvent(event);
}
to your activity, then simply pass your view on ACTION_DOWNand null on ACTION_UP. it's not exactly pretty, but it works. i'm sure there's a proper way to do this though.
finally learned the correct way to do this: requestDisallowInterceptTouchEvent
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
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;
}