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();
}
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 one customView and customView added in scroll view. Now i want differentiate both touch events. My problem is when try to scroll, customView also getting touch event and when i try to change in customView, scroll view getting events.
How can we stop customView touch event when scrolling.
How can we stop scroll touch events when customView wants events.
Thanks in advance
You can set touch listener to child view and then in onTouch() event, you can block intercept touch event of parent.
i.e.
v.setOnTouchListener(new OnTouchListener() {
// Setting on Touch Listener for handling the touch inside ScrollView
#Override
public boolean onTouch(View v, MotionEvent event) {
// Disallow the touch request for parent scroll on touch of child view
v.getParent().requestDisallowInterceptTouchEvent(true);
return false;
}
});
About the second question, I don't know exactly what you're doing with customview but maybe you'd like to use click events instead because it's rather not user friendly to use different logic in ontouch and onclick as it will always fire up unexpectedly.
boolean isScrolling = false;
myScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() { #Override public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY)
isScrolling = scrollX != oldScrollX;
//for vertical scrolling
});
//then onTouchListener
if(!isScrolling){//Do operations on non scrolled state}
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.
today I got a problem with touch event handling on android custom views.In this case i have created parent view call weekview and chiled call weekdayview.i want implement touch event like singleTap,LongPress in child view only and when i swipe on parent or child i wanna scroll parent view.when i implement touch event in both view it dose not work.
can anyone help me on this.It's really helpful to me.
Thank you
class ChildView extends View {
public void setGestureDetector (GestureDetector g)
{
gesture = g;
}
#Override
public boolean onTouchEvent (....)
{
return gesture.onTouchEvent (....); // touch event will dispatch to gesture
}
}
class ParentView extends View implements GestureDetector.OnGestureListener {
gesture = new GestureDetector (this);
child = new ChildView (...);
child.setGestureDetector (gesture);
#Override
public boolean onTouchEvent (..)
{
// handle your parent touch event here
}
public boolean onDown (...)
{
return true;
}
public boolean fling (...)
{
// here to handle child view fling
}
}
This is peso-code (not real android java) to show you the concept to use GestureDetector, you can deal with all events from your child view in your parent View. As I tested on my android phone, onTouchEvent in ChildView didn't recognize ACTION_UP very well, so even you swipe your Child View, sometimes fling will not work (it depends on ACTION_UP).
So if you want to write more accurate swipe on your Child View, better write your owen Gesture Detect class, and in your ChildView, you can do this -
float oldX;
float distanceX;
public boolean onTouchEvent (MotionEvent event...)
{
if (event.getAction == MotionEvent.ACTION_DOWN) {
// handle down
oldX = event.getX ();
}
if (event.getAction == MotionEvent.ACTION_MOVE {
// handle move
distanceX = event.getX() - oldX; // more accurate
gesture.onSwipe (distanceX); // your own gesture class
}
}
Set your swipe velocity (to detect user's intention to swipe) and override the onTouchEvent() in your child view.
Here, call super.onTouchEvent() and return, which calls your parent view. Handle the events in the parent view.
I have a ListView in Android and I would like to have onTouchListeners for the row views and the parent ListView itself. I'd like to respond to single taps and long presses on the rows individually, but to flings on the ListView as a whole. (I can set the onFling method for each of the rows individually, but it's not robust, since the user can move his finger across rows.) I'm able to successfully set an onTouchListener for the rows and the ListView separately, but when I set both, the ListView listener never triggers - only the row listeners. This is the case whether I return true or false in the methods of the row listener. Does anybody know how to trigger onTouchListeners for both a view and its parent, given that they occupy the same pixels on the screen?
The relevant code for the ListView:
In the activity onCreate method:
mListViewDetector = new GestureDetector(new ListViewGestureListener());
mListViewListener = new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
return mListViewDetector.onTouchEvent(event);
}
};
mListView = getListView();
mListView.setOnTouchListener(mListViewListener);
And the custom GestureListener class (defined as a nested class in the activity):
class ListViewGestureListener extends SimpleOnGestureListener {
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.i("ListViewGestureDetector","onFling");
return false;
}
}
The relevant code for the individual rows in the Listview:
In the bindView method of the the adapter class:
TextView textView;
textView.setOnTouchListener(new ListsTextViewOnTouchListener());
And the custom onTouchListener class (defined as a nested class in the adapter class):
class ItemsTextViewOnTouchListener implements OnTouchListener {
public boolean onTouch (View v, MotionEvent event){
switch(event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
//stuff
return false;
case MotionEvent.ACTION_CANCEL:
//stuff
return false;
default:
break;
}
return false;
}
}
Pvans,
The answer is a little tricksy, but that capability is built in. onTouchEvent() handles the TouchEvent for whichever object(s) it is a listener for. There are then two options for accomplishing what you would like.
1) You can override onInterceptTouchEvent() on the parent View and handle your fling there. I find that this works great for large Views with smaller child Views. I do not prefer this method for Lists (to be frank). onInterceptTouchEvent() is awesome at taking the Touch away from the children, but if you do this, you would have to handle both flinging left/right and scrolling horizontally.
2) You can also create a GestureDetector that is actually an invisible overlay (sort of). If you forward your onTouchEvents to it, you will still get the desired behavior, and you don't have to worry about handling your scrolling manually. In this case, you simply send the TouchEvent to the Detector and if it returns true, your List does nothing and your OnFling does the work. If it returns false, it wasn't handled so it must belong to the List or one of its children.
Hope this helps,
FuzzicalLogic