onTouch: detect ACTION_UP without blocking other touch events - android

I have a custom view which acts as a button. I am drawing all the canvas myself. Now I'm making an outline when ACTION_DOWN and remove it after ACTION_UP or ACTION_CANCEL.
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("test", "ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
Log.e("test", "ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e("test", "ACTION_CANCEL");
break;
}
return true;
}
This can work for me, except now it is blocking another gesture behind this view which is detecting ACTION_MOVE (scroll left).
If I return false, then it is working fine but now ACTION_UP is not called.
I want to call ACTION_UP if finger is lifted, but pass events down otherwise.

Have you tried overriding dispatchTouchEvent?
https://developer.android.com/reference/android/view/View.html#dispatchTouchEvent(android.view.MotionEvent)
UPDATE:
So touch events are a bit of a beast. The rundown of it is this...
They bubble up at first from your root container in your Activity. This is done by calling dispatchTouchEvent and then onInterceptTouchEvent assuming intercepting wasn't blocked by a child view.
If no view intercepts the event, it will bubble to the leaf node (such as a button) where onTouch is called. If the node doesn't handle it (returns true) its parent gets a chance and so on.
This means that you can use dispatchTouchEvent or onInterceptTouchEvent to spy on touch events without changing the behavior. Unless you're actually going to intercept the event I suggest using dispatchTouchEvent as it's guaranteed to run whereas intercepting may be blocked (example: DrawerLayout will intercept touch events near the edge in order to open the drawer).
So the final result is:
public class MyView extends Button {
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
Log.e("test", "ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
Log.e("test", "ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e("test", "ACTION_CANCEL");
break;
}
return super.dispatchTouchEvent(event);
}
}
UPDATE:
Sorry, so I've been under the impression for some reason (mostly my poor reading) that we were dealing with the parent. Here's what I would do...
Go ahead and implement onTouch and return true to consume all the events. This means that any touch events that start on your view will be eaten up. What we'll do then is translate the point to the parent's coordinate space and manually pass the touch event up, it'll look like this inside your custom view...
private boolean passingTouchEventToParent = true;
final private Rect hitRect = Rect();
#Override
public boolean onTouch(MotionEvent event) {
// Handle your custom logic here
final ViewParent viewParent = getParent();
if (passingTouchEventToParent &&
viewParent != null &&
viewParent instanceof View) {
// Gets this view's hit rectangle in the parent's space
getHitRect(hitRect);
event.offsetLocation((float) hitRect.left, (float) hitRect.top);
passingTouchEventToParent = viewParent.onTouchEvent(event);
}
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
// Motion event finished, reset passingTouchEventToParent
passingTouchEventToParent = true;
}
return true;
}

Related

How to programmatically cancel the OnTouchListener callback even if the user haven't released the touch from a view in Android?

I am working on an Audio Recording App. It works in a way that when the user presses and moves the record button, the button moves along with the finger. I have created a boundary and when the finger crosses that boundary I want the button to perform the hide() animation and get back to it orginal position.
The whole process works fine if the MotionEvent.ACTION_UP or MotionEvent.ACTION_CANCEL event is occurred, but the hide() operation is not occurring even if the touch crosses the boundary. The button plays a back and forth motion sometimes when it is outside the boundary. The touch event is still being called even if I set the visibility of the view to false.
I get the output in the logcat as well (Log.e("MSG","boundary crossed");).
This is the code:
int recordButtonStartX;
microPhoneListner=new View.OnTouchListener() {
#Override
public boolean onTouch(View v, final MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
recordButtonStartX = (int) event.getX();
this.floatingRecordButton.display(event.getX());
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
this.floatingRecordButton.hide(event.getX());
break;
case MotionEvent.ACTION_MOVE:
int tempX = (int) event.getX();
if ((recordButtonStartX - tempX) > 200) {
Log.e("MSG","boundary crossed");
this.floatingRecordButton.hide(event.getX());
}
else
{
this.floatingRecordButton.moveTo(event.getX());
}
break;
}
recordMsgButton.setOnTouchListener(microPhoneListner);
To release the onTouchListener for any View set the listener to null.
recordMsgButton.setOnTouchListener(null);
or
After your condition satisfied you can set other listener to that View.
Make another listener
public final OnTouchListener mTouchListener = new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent rawEvent) {
return false;
}
};
When you want to disable the listener then set other listener to that view
v.setOnTouchListener(mTouchListener);

Unable to detect ACTION_MOVE or ACTION_UP touch events

I am having trouble detecting the MotionEvent.ACTION_MOVE & MotionEvent.ACTION_UP events. I am intending to have a button when you press and hold, it automatically decreases an associated value, however I have not gotten to that point in coding because I cannot detect the UP event.
for (int position = 0; position < mListItems.size(); position++) {
LayoutInflater inflater = getLayoutInflater();
PilotSkillRow row = (PilotSkillRow) inflater.inflate(R.layout.pilotskillrow, mSkillListView, false);
Button skillminus = (Button) row.findViewById(R.id.skillminus);
skillminus.setOnTouchListener(new SkillButtonTouchListener(position, false));
....
}
I understand returning true on the on touch events means that the later ontouch events such as ACTION_MOVE and ACTION_UP should fire.
private class SkillButtonTouchListener implements OnTouchListener {
public SkillButtonTouchListener(int pos, boolean plus) {
...
}
public boolean onTouch(View view, MotionEvent motionevent) {
int action = motionevent.getActionMasked();
switch (action)
{
case MotionEvent.ACTION_MOVE:
Log.e("a", "MOVE EVENT");
....
return true;
case MotionEvent.ACTION_UP:
Log.e("a", "UP EVENT");
....
break;
case MotionEvent.ACTION_DOWN:
Log.e("a", "DOWN EVENT");
....
return true;
default:
break;
}
return false;
}
}
However, when I run the code, the DOWN even it displayed, but the MOVE and UP EVENTS simply are never displayed. Could it be related to the fact I have an inflated layout?
Anyone have any ideas what I am doing wrong?
Update: If I use the android debugger to connect to the button to check what is happening, the UP event fires when I step through after pressing the button. Probably because there is another process consuming the UP event?
Update 2: The problem is that the textview inside an inflated layout refuses to update. When I perform an invalidate event on the adapter, it will stop the currently processing touch events (no errors). I have another textview which I am able to manually update without the need of the adapter and this does not cause any problems. So it seems to be a problem specific towards RelativeLayout. I have a workaround to only invalidate the data during the ACTION_UP event but I'd rather update the textview on the fly.

onInterceptTouchEvent, onTouchEvent only see ACTION_DOWN

I have a top level ViewGroup, which I call SliderView, in which I want to detect swiping. This is mostly working, but one weird failure persists.
The essence of SliderView is to override onInterceptTouchEvent and, once the user is actually swiping, return "true" to prevent other views from seing the MotionEvent. Here is a snip of code:
public class SliderView extends ViewGroup
{
enum MoveState { MS_NONE, MS_HSCROLL, MS_VSCROLL };
private MoveState moveState = MoveState.MS_NONE;
... other code ...
public boolean onInterceptTouchEvent(MotionEvent e)
{
final int action = e.getAction();
switch (action & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
moveState = MoveState.MS_NONE;
break;
case MotionEvent.ACTION_MOVE:
if (moveState == MoveState.MS_NONE)
{
if (motion is horizontal)
{
moveState = MoveState.MS_VSCROLL;
return true;
}
else
moveState = MoveState.MS_VSCROLL; // let child window handl MotionEvent
}
else if (moveState == MoveState.MS_HSCROLL)
return true; // don't let children see motion event.
}
return super.onInterceptTouchEvent (e);
}
... other code ...
}
It is my understanding that my SliderView (which is the outermost view) should always recevie onInterceptTouchEvent. In one of my tests, where the top level child is a However, in the following case, this appears not to be.
When the top level child is a ScrollView, onInterceptTouchEvent gets ACTION_MOVE and my code does what I want. In another case, where the top level child is a LinearLayout, it fails sometimes: it always gets ACTION_DOWN but gets ACTION_MOVE only if the user touches a widget inside the LinearLayout; if touching blank area, only ACTION_DOWN comes through.
I'll note that it behaves as if the fail-case touches are happening outside the SliderView. However, if that were the case, why would I get the ACTION_DOWN events?
Second note: looking at the source code for ScrollView, I see it checking for "inChild"; I have not figured out what that's for and how it might be relevant.
Due to the answer of user123321 here
onInterceptTouchEvent only get called if the parent has a child view which returns "true" from onTouchEvent. Once the child returns true, the parent now has a chance to intercept that event
All you need is to call
requestDisallowInterceptTouchEvent(true);
on the parent view, like this -
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
view.getParent().requestDisallowInterceptTouchEvent(true);
switch(motionEvent.getActio){
}
return false;
}
From Android developer's reference (http://developer.android.com/reference/android/view/ViewGroup.html#onInterceptTouchEvent(android.view.MotionEvent)):
"2.
.... 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."
Maybe because your onTouchEvent always returns true..?
When intercepting onTouchEvent, there are two things to do to properly intercept the touches (all else being default).
Return false in onInterceptTouchEvent()
#Override
public boolean onInterceptTouchEvent(MotionEvent me) {
return false;
}
Return true in onTouchEvent()
#Override
public boolean onTouchEvent(MotionEvent me) {
switch (me.getAction()) {
case MotionEvent.ACTION_DOWN:
log("MotionEvent.ACTION_DONE");
break;
case MotionEvent.ACTION_MOVE:
log("MotionEvent.ACTION_MOVE");
break;
case MotionEvent.ACTION_CANCEL:
log("MotionEvent.ACTION_CANCEL");
userActionDown = false;
break;
case MotionEvent.ACTION_UP:
log("MotionEvent.ACTION_UP");
break;
}
return true;
}
Then, for your case (and others). Do all your calculations in the onTouchEvent() as shown above. The onInterceptTouchEvent() will only be called once for the ACTION_DOWN. But, the onTouchEvent will also get the ACTION_DOWN event, and you'll need to return true there, rather than the super.
For more information regarding onInterceptTouchEvent(): http://developer.android.com/reference/android/view/ViewGroup.html#onInterceptTouchEvent(android.view.MotionEvent)
ps - When you ask questions here, you should also write the description of what you are trying to do. You might quite possibly find much better ways of doing things. For your case of navigation, the real answer you are looking for is ViewPager. It works great and is very easy to implement. You should also check out some other easy navigation patters that Android has to offer developers: link.

GridView problems with touchlisteners

I'm trying to create a custom GridView but i'm having troubles with the touch listeners.
What i want to do:
Create a GridView with custom Views.
Longpress on an item so it becomes 'editable'.
Drag the view horizontal or vertical to move it's position in the GridView.
Here's where i'm having trouble:
I'm implementing GestureDetector.OnGestureListener for the longpress functions, because for some reason using the gridview.setOnItemLongClickListener() isn't working when overriding the onTouchEvent() of the GridView itself (Which i need for the dragging part). So everything is fine at this point. Now i only need to know when the longpress is finished. So i though: "Well this shouldn't be hard." I couldn't have be more wrong. I've fiddled around for quite some time with this and it looks like using different touch events isn't helping me :/
When stepping through the onTouchEvent() i noticed that only 1 action is given: MotionEvent.ACTION_DOWN. So what am i doing wrong? i need the MotionEvent.ACTION_UP...
Found the culprit:
i was doing something like this
#Override
public boolean onTouchEvent(MotionEvent event) {
// Give everything to the gesture detector
boolean retValue = gestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE :
onMove();
break;
case MotionEvent.ACTION_UP :
onUp();
break;
case MotionEvent.ACTION_CANCEL:
onCancel();
break;
}
return retValue;
}
i think retValue was always returning false so no other events were triggered.
this fixed the issue:
#Override
public boolean onTouchEvent(MotionEvent event) {
// Give everything to the gesture detector
gestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE :
onMove();
break;
case MotionEvent.ACTION_UP :
onUp();
break;
case MotionEvent.ACTION_CANCEL:
onCancel();
break;
}
return true;
}

How to get a continuous Touch Event?

My class extends View and I need to get continuous touch events on it.
If I use:
public boolean onTouchEvent(MotionEvent me) {
if(me.getAction()==MotionEvent.ACTION_DOWN) {
myAction();
}
return true;
}
... the touch event is captured once.
What if I need to get continuous touches without moving the finger?
Please, tell me I don't need to use threads or timers. My app is already too much heavy.
Thanks.
Use if(me.getAction() == MotionEvent.ACTION_MOVE). It's impossible to keep a finger 100% completely still on the screen so Action_Move will get called every time the finger moves, even if it's only a pixel or two.
You could also listen for me.getAction() == MotionEvent.ACTION_UP - until that happens, the user must still have their finger on the screen.
You need to set this properties for the element
android:focusable="true"
android:clickable="true"
if not, just produce the down action.
Her is the simple code snippet which shows that how you can handle the continues touch event. When you touch the device and hold the touch and move your finder, the Touch Move action performed.
#Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
if(isTsunami){
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Write your code to perform an action on down
break;
case MotionEvent.ACTION_MOVE:
// Write your code to perform an action on contineus touch move
break;
case MotionEvent.ACTION_UP:
// Write your code to perform an action on touch up
break;
}
}
return true;
}
Try this. It works to me:
public static OnTouchListener loadContainerOnTouchListener() {
OnTouchListener listener = new OnTouchListener(){
#Override
public boolean onTouch(View v, MotionEvent event) {
LinearLayout layout = (LinearLayout)v;
for(int i =0; i< layout.getChildCount(); i++)
{
View view = layout.getChildAt(i);
Rect outRect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
if(outRect.contains((int)event.getX(), (int)event.getY()))
{
Log.d(this.getClass().getName(), String.format("Over view.id[%d]", view.getId()));
}
}
}
Remember: the listener you´ll set must be a container layout (Grid, Relative, Linear).
LinearLayout layout = findViewById(R.id.yourlayoutid);
layout.setOnTouchListener(HelperClass.loadContainerOnTouchListener());
This might help,
requestDisallowInterceptTouchEvent(true);
on the parent view, like this -
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
view.getParent().requestDisallowInterceptTouchEvent(true);
switch(motionEvent.getAction()){
}
return false;
}
I was making a game with a custom view used as a thumb control. . . here is what I did
float x = 0, y = 0;
#Override
public boolean onTouchEvent(MotionEvent event) {
x = event.getX();
y = event.getY();
// handle touch events with
switch( event.getActionMasked() ) {
case MotionEvent.ACTION_DOWN :
if(cont)
{
// remove any previous callbacks
removeCallbacks(contin);
// post new runnable
postDelayed(contin, 10);
}
invalidate();
return true;
case MotionEvent.ACTION_MOVE :
if(!cont && thumbing != null)
{
// do non-continuous operations here
}
invalidate();
return true;
case MotionEvent.ACTION_UP :
// set runnable condition to false
x = 0;
// remove the callbacks to the thread
removeCallbacks(contin);
invalidate();
return true;
default :
return super.onTouchEvent(event);
}
}
public boolean cont = false;
// sets input to continuous
public void set_continuous(boolean b) { cont = b; }
public Runnable contin = new Runnable()
{
#Override
public void run() {
if(x != 0)
{
// do continuous operations here
postDelayed(this, 10);
}
}
};
A quick note however, make sure in your main activity that is calling this view removes the callbacks manually via the onPause method as follows
#Override
protected void onPause() {
if(left.cont) left.removeCallbacks(left.contin);
if(right.cont) right.removeCallbacks(left.contin);
super.onPause();
}
That way if you pause and come back touch events aren't being handled twice and the view is free from it's thread's overhead.
** tested on Samsung Galaxy S3 with hardware acceleration on **
All these answer are partially correct but they do not resolve in the right way the problem.
First of all, for everyone out there that decide to track when the event is ACTION_MOVE. Well that works only guess when? When user move his finger, so could if you decide to implement a custom thumb control is okay but for a normal custom button that's not the case.
Second, using a flag inside ACTION_DOWN and check it in ACTION_UP seems the logic way to do it, but as Clusterfux find out if you implement a while(!up_flag) logic you get stuck into troubles ;)
So the proper way to do it is mentioned here:
Continuous "Action_DOWN" in Android
Just keep in mind that if the logic you're going to write during the continuous press has to modify the UI in some way, you have to do it from the main thread in all the other cases it's better use another thread.
You can use the below code snippet as a reference in which I used the background to detect if the screen is held or not...
Main_Layout.setOnTouchListener(new View.OnTouchListener() {
#SuppressLint("ResourceAsColor")
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
Main_Layout.setBackgroundColor(R.color.green);
event.setAction(MotionEvent.ACTION_DOWN);
break;
default:
Main_Layout.setBackgroundColor(R.color.blue);
break;
}
return false;
}
});

Categories

Resources