Android MotionEvent : Transfer between views - android

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.

Related

How to pass a click event to RecyclerView's items?

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

Android Touch Listener

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.

Passing touch event (move/swipe) to parent, but keeping click event on current view

I have a listview with custom adapter. Each row has some text and a clickable FrameLayout, that has onClickListener set on it. Rows can be deleted with swipe (like gmail or hangouts). I am using Roman Nurik's SwipeToDismiss library. The problem is that the clickable FrameLayout takes half of space of row, so it is very hard to swipe the row away, because it consumes the MOVE event.
I have read that I can pass event to the parent view with extending FrameLayout and overriding the dispatchTouchEvent method. I have tried that, but then all events are passed and therefore FrameLayout is no longer clickable.
I also tried this:
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
return false;
}
return super.dispatchTouchEvent(ev);
}
but it looks like once the view "got" the DOWN event, it won't pass it to parent anymore.
My question:
What should I do, to keep onClick event on the current view, but pass MOVE events to the parent view?
I got it working. I extended FrameLayout and overrided method dispathTouchEvent:
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
//if move event occurs
long downTime = SystemClock.uptimeMillis();
long eventTime = SystemClock.uptimeMillis() + 100;
float x = ev.getRawX();
float y = ev.getRawY();
int metaState = 0;
//create new motion event
MotionEvent motionEvent = MotionEvent.obtain(
downTime,
eventTime,
MotionEvent.ACTION_DOWN,
x,
y,
metaState
);
//I store a reference to listview in this layout
listview.dispatchTouchEvent(motionEvent); //send event to listview
return true;
}
return super.dispatchTouchEvent(ev);
}
Now the FrameLayout is clickable, but after MOVE happens, it creates a new MotionEvent and sends it to ListView, where the content is scrolled or swiped away. I should probably add some +- to MOVE event, because when you press with finger, you usually do some MOVE events with it.

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

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

Android: ViewGroup, how to intercept MotionEvent and then dispatch to target or eat it on demand?

Given that there is a ViewGroup with several children. As for this ViewGroup, I'd like to have it managing all MotionEvent for its all children, which says VG will
1. be able to intercept all events before they get dispatched to target (children)
2. VG will first consume the event, and determine if will further dispatch event to target child
3. DOWN, MOVE, UP, I'd like to see them as relatively independent, which means VG could eat DOWN, but give MOVE and UP to children.
I've read SDK guide "Handling UI Event", I knew event listeners, handlers, ViewGroup.onInterceptTouchEvent(MotionEvent), and View.onTouchEvent(MotionEvent).
Here's my sample,
#Override
public boolean onInterceptTouchEvent (MotionEvent event) {
if (MotionEvent.ACTION_DOWN == event.getAction()) {
return true;
}
return super.onInterceptTouchEvent(event);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (MotionEvent.ACTION_DOWN == event.getAction()) {
return true;
}
else {
if (!consumeEvent(event)) {
// TODO: dispatch to target since I didn't want to eat event
//return this.dispatchTouchEvent(event); // infinite loop!!!
}
}
return super.onTouchEvent(event);
}
To be able to eat some events, I have to return true in above both methods when DOWN event occurred, because SDK said so. Then I could see MOVE and up in onTouchEvent. However, in my case, I've no idea about how to dispatch event to children.
Above dispatchTouchEvent led to infinite loop, which was understandable, since VG itself might be the target. I can't tell which would be target at that moment, MotionEvent didn't give a hint, so dispatchTouchEvent was totally useless.
Anyone help me out? Thanks.
There is no easy way to find the source View from onInterceptTouchEvent, nor there is a way to "dispatch" these events. You can dispatch KeyEvents, but not MotionEvents.
A common way to deal with MotionEvents (e.g., for drag and drop) is to handle the MotionEvent.ACTION_DOWN events by the different Views (through the onTouch callback after implementing OnTouchListener), and the MotionEvent.ACTION_MOVE events through the parent Viewgroup's onInterceptTouchEvent method.
But some LOCs say a lot more than a bunch of words. There's a very nice example of what I'm saying here: http://doandroids.com/blogs/tag/codeexample/
If you handle the ACTION_DOWN event in the View itself, then you can store which View started it elsewhere and use that variable for further actions. The Event is bound to the same View until is finished by an ACTION_UP or an ACTION_CANCEL actions.
If you need to keep track the View and execute an action on the ViewGroup during the ACTION_DOWN, then I suggest you to add a public method in your ViewGroup (e.g. public boolean handleActionDown (View v, MotionEvent e) that will be called from the onTouch callback

Categories

Resources