as the documentation for TouchGesture says as follows:
Capturing touch events for a single view
As an alternative to onTouchEvent(), you can attach an View.OnTouchListener object to any View object using the setOnTouchListener() method. This makes it possible to to listen for touch events without subclassing an existing View. For example:
View myView = findViewById(R.id.my_view);
myView.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
// ... Respond to touch events
return true;
}
});
Beware of creating a listener that returns false for the ACTION_DOWN event. If you do this, the listener will not be called for the subsequent ACTION_MOVE and ACTION_UP string of events. This is because ACTION_DOWN is the starting point for all touch events.
But returning false for the onTouch() method calls subsequent events ACTION_MOVE AND ACTION_UP and returning true is not calling the following events such as ACTION_MOVE AND ACTION_CANCEL. This look counter part from the documentation.
my code :
/**
* Setting Touch Listener to Tabs <br/>
* ReSelecting the tabs calls the touch listener and open the Default/Initial Screen for the tab.
*/
protected void setTabListeners() {
if (mTabHelper.getTabHost() != null) {
final TabWidget tabWidget = mTabHelper.getTabHost().getTabWidget();
int tabsCount = tabWidget.getChildCount();
for (int i = 0; i < tabsCount; i++) {
mLogger.info("count = " + i);
tabWidget.getChildAt(i).setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
mLogger.info("event "+event.getAction());
return false;
}
});
}
}
Returning false means that you won't have consumed the event and it's up for grabs by anything else along the chain.
I would recommend that you take the time to watch the talk by Dave Smith on how touch events work on Android and how they are passed down through to child views to consume the events. It's actually the inverse of what most people would expect.
This should clear up any other questions you have.
Related
I know it might be a silly question but I need to know after I implement this code:
recyclerView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
Why the recyclerView is still scrolling when the listener returns false? Or more precisely where is the scrolling behavior processed and handled?
I know that return true means the touch event is consumed and false means the touch event should get passed to the next view in view hierarchy. In my mind (which is possibly wrong), the return type shouldn't change the view behavior. Because when you don't process the onTouchListener, it means no touch event (including scrolling behavior) is processed so the recyclerView shouldn't be scrolling no matter the return type is true or false. What is wrong in my perception? I hope I'm clear enough.
I know that return true means the touch event is consumed and false means the touch event should get passed to the next view in view hierarchy
This is not true, the correct order when a View handle a touch event is:
View.dispatchTouchEvent() will be called first
Sends event to View.OnTouchListener.onTouch() if exits
If not consumed, process View.onTouchEvent()
In your case because your return false in View.OnTouchListener.onTouch(), it means you do not consume the event, so the event will be routed to View.onTouchEvent() of RecyclerView, which explains why the RecyclerView is still scrolling.
Solution 1
Return true in View.onTouchListener.onTouch() to show that the RecyclerView will consume all touch events.
recyclerView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
// I will consume all touch events,
// so View.onTouchEvent() will not be called.
return true;
}
});
Solution 2
Create a sub-class that extends from RecyclerView and return false in View.onTouchEvent() to show that the RecyclerView don't show interested in any touch event.
public class MyRecyclerView extends RecyclerView {
public MyRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onTouchEvent(MotionEvent e) {
// I don't show interested in any touch event.
return false;
}
}
This is a great presentation about Android Touch System, you should take a look.
I have an ordinary RecyclerView, and on top of it a transparent View that implements GestureListener, which basically have the same size of the RecyclerView.
The GestureListener will listen to scroll and fling gestures, and pass this MotionEvent to the RecyclerView underneath it.
I have already made the RecyclerView able to scroll and fling. However, I can't find a way to pass a click event down to the RecyclerView's items as well.
I already know that this is because ACTION_DOWN is consumed in the GestureListener. In fact, GestureListener has a onSingleTap() method for you to override, and this method was called whenever I perform a click.
According to this post, I tried to set an OnTouchListener to my itemView and listen to ACTION_UP events. However, the onTouch() method is never called.
Below is how I do it:
1. Create a callback in the transparent GestureListener
#Override
public boolean onSingleTapUp(MotionEvent e) {
if (scrollDetector == null) return false;
scrollDetector.onSingleTap(e);
return true;
}
Configure the callback in the activity, and pass the MotionEvent to the RecyclerView
#Override
public void onSingleTap(MotionEvent e) {
mRecyclerView.onTouchEvent(e);
}
Set OnTouchListener to the itemView in the adapter:
itemView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
v.performClick();
return true;
}
return false;
}
});
Using debugger, I can see that mRecyclerView.onTouchEvent(e) was called; but the onTouch() of itemView was not called.
So... How should I correctly pass the MotionEvent to the itemView?
You may ask - "Why do you place a GestureListener on top of the RecyclerView?"
This is because I need to change the height of the RecyclerView when the RecyclerView is scrolled. However, if I do this using RecyclerView's addOnScrollListener, the value of dy will fluctuate between negative and positive values, because dy is affected by its height as well. And the fluctuation will also be reflected to the UI.
Therefore I need a scroll detector that does not change its height when scrolled, and just pass the scroll and fling values to RecyclerView by programmatically calling scrollBy() and fling().
You should known that recyclerview's event.If recyclerview can move when you scroll views,it will call onInterceptTouchEvent() and return true to intercept event.So you can't get the ACTION_MOVE event.Maybe you should rewrite the recyclerview's onInterceptTouchEvent() and return false. Then you can get all the event in your itemView's methods.
Stupid me. I should use dispatchTouchEvent(MotionEvent e) instead of onTouchEvent(MotionEvent e).
However, this is not enough.
Simply calling dispatchTouchEvent(e) using the MotionEvent from GestureListener is not working, because that e is an ACTION_UP event.
To simulate a click, you need both ACTION_DOWN and ACTION_UP.
And itemView does not need to set OnTouchListener since you have already simulate
Code:
#Override
public void onSingleTap(MotionEvent e) {
long downTime = SystemClock.uptimeMillis();
long upTime = downTime + 100;
MotionEvent downEvent = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,
e.getX(), e.getY(), 0);
mRecyclerView.dispatchTouchEvent(downEvent);
MotionEvent upEvent = MotionEvent.obtain(upTime, upTime, MotionEvent.ACTION_UP,
e.getX(), e.getY(), 0);
mRecyclerView.dispatchTouchEvent(upEvent);
downEvent.recycle();
upEvent.recycle();
}
I have implemented an OnTouchListener in Android 5.1 which disables touch input and only allows stylus input (or any other input than touch) in the following way:
View.OnTouchListener touchListener = new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
if ((event.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER)) {
// Touch input
return true;
} else {
return false;
}
}
};
This works great. Returning true does not pass the touch event to underlying activity, that is what I want.
According to what I have read, returning false is passing the event to underlying activity but only ACTION_DOWN should be processed and ACTION_MOVE and ACTION_UP should no more be processed (because somehow the chain is stopped after ACTION_DOWN when returning false). In my case ACTION_MOVE and ACTION_UP are still called and processed by the OnTouchListener although I'm returning false.
Why is this the case? I want to have also the ACTION_UP and ACTION_MOVE events, that is not the problem but I don't understand the behavior according to what I have read.
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.
When application is started I run a custom pop-up till a user touches the screen. When screen is touched I catch it with event onTouch() and cancel the pop-up. From this point I don't need the event anymore.
The problem is the event is alive and continues to jump up every time a user touches the screen.
Is there any way to unsubscribe from this event? Something like in c# -= eventName.
The code is below:
#Override
public boolean onTouch(View v, MotionEvent event) {
if (!_stopToast)
{
_hintToast.cancel();
_stopToast = true;
}
return false;
}
There's no such method (lets say removeTouchListener or similar) which will help you to remove an already defined touch listener from a view. Setting null to setOnTouchListener won't help too. What you can do is to create a new object reference of OnTouchListener class which does nothing and set it in setOnTouchListener. For example:
public final OnTouchListener dummyOnTouchListener = new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent rawEvent) {
return false;
}
};
And simply use it as below:
yourView.setOnTouchListener(dummyOnTouchListener);