I'm implementing a swipe gesture similar to the one in Android Gmail app where you drag across an item to archive it.
I attach a touch listener to the item view and a gesture detector and listener to listen to touch events like this:
GestureDetector.OnGestureListener gl;
gl = new GestureDetector.SimpleOnGestureListener() {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) {
// ... handle horizontal scrolling
return true;
}
};
final GestureDetector gd = new GestureDetector(getContext(), gl);
itemView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent e) {
gd.onTouchEvent(e);
// ... handle TOUCH_UP and TOUCH_CANCEL
return true;
}
});
The problem is that as soon as I start dragging vertically, or go outside the bounds of the item view, the gesture is cancelled and list view starts handling it for normal vertical list scrolling. I need the gesture to not cancel until the user releases the finger, even if it moves outside the table cell, like it's done in Gmail app.
How do I have it not cancelled?
Related
I have a horizontal RecyclerView. I need to scroll it but to disable fling by swipe gesture.
Originally all work is done at onTouchEvent method and I dont know how to disable it without rewriting all touch handling
You can use a GestureDetector with a SimpleOnGestureListener to capture fling events and decide whether or not to allow them.
RecyclerView recycler = findViewById(R.id.recycler);
GestureDetector detector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// return true if you want to stop the fling
// return false if you want to allow the fling
return true;
}
});
recycler.setOnTouchListener((v, event) -> detector.onTouchEvent(event));
GestureDetector.OnGestureListener#onFling()
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 was struggling with adding a gesture detector to a subview in my project. Do I override the parent's onTouchEvent or the child's onTouchEvent? Do I make an OnTouchListener and add the gesture detector there? The documentation shows an example for how to add a gesture detector to the activity itself but it is not clear how to add it to a view. The same process could be used if subclassing a view (example here), but I want to add the gesture without subclassing anything.
This is the closest other question I could find but it is specific to a fling gesture on an ImageView, not to the general case of any View. Also there is some disagreement in those answers about when to return true or false.
To help myself understand how it works, I made a stand alone project. My answer is below.
This example shows how to add a gesture detector to a view. The layout is just a single View inside of an Activity. You can use the same method to add a gesture detector to any type of view.
We will add the gesture detector to the green View.
MainActivity.java
The basic idea is to add an OnTouchListener to the view. Normally we would get all the raw touch data here (like ACTION_DOWN, ACTION_MOVE, ACTION_UP, etc.), but instead of handling it ourselves, we will forward it on to a gesture detector to do the interpretation of the touch data.
We are using a SimpleOnGestureListener. The nice thing about this gesture detector is that we only need to override the gestures that we need. In the example here I included a lot of them. You can remove the ones you don't need. (You should always return true in onDown(), though. Returning true means that we are handling the event. Returning false will make the system stop giving us any more touch events.)
public class MainActivity extends AppCompatActivity {
private GestureDetector mDetector;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// this is the view we will add the gesture detector to
View myView = findViewById(R.id.my_view);
// get the gesture detector
mDetector = new GestureDetector(this, new MyGestureListener());
// Add a touch listener to the view
// The touch listener passes all its events on to the gesture detector
myView.setOnTouchListener(touchListener);
}
// This touch listener passes everything on to the gesture detector.
// That saves us the trouble of interpreting the raw touch events
// ourselves.
View.OnTouchListener touchListener = new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
// pass the events to the gesture detector
// a return value of true means the detector is handling it
// a return value of false means the detector didn't
// recognize the event
return mDetector.onTouchEvent(event);
}
};
// In the SimpleOnGestureListener subclass you should override
// onDown and any other gesture that you want to detect.
class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
#Override
public boolean onDown(MotionEvent event) {
Log.d("TAG","onDown: ");
// don't return false here or else none of the other
// gestures will work
return true;
}
#Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.i("TAG", "onSingleTapConfirmed: ");
return true;
}
#Override
public void onLongPress(MotionEvent e) {
Log.i("TAG", "onLongPress: ");
}
#Override
public boolean onDoubleTap(MotionEvent e) {
Log.i("TAG", "onDoubleTap: ");
return true;
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
Log.i("TAG", "onScroll: ");
return true;
}
#Override
public boolean onFling(MotionEvent event1, MotionEvent event2,
float velocityX, float velocityY) {
Log.d("TAG", "onFling: ");
return true;
}
}
}
It is a quick setup to run this project, so I recommend you try it out. Notice how and when the log events occur.
short version in kotlin to detect double tap only for a view:
val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent?): Boolean {
Log.d("myApp", "double tap")
return true
}
})
myView.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
and don't forget to make myView clickable
I'd like to respond to horizontal "fling" gestures on individual cells in a vertically scrolling ListView. Currently I've accomplished this by using a GestureDetector for each cell view in the list.
I'm noticing, though, that it's much harder to actually get a horizontal "fling" to register with one of the cell views than if they were just stuck in a non-scrolling linear layout.
For instance, if I tap down inside a cell then drag my finger "up and to the right" fairly quickly this is recognized as a fling in the non-scrolling case, but isn't recognized in the scrolling case.
I've experimented with sub-classing ListView and overriding onInterceptTouchEvent, then I can't seem to get it right. What I would like to have happen is for gestures that will eventually be recognized as "flings" on a child view to be ignored by the scroll view. I would want to limit these based on the "angle" of the gesture, i.e. the ratio of the Y distance to the X distance. If that ratio is sufficiently high then its a "vertical" fling and the ListView should handle it. If that ratio is sufficiently low then it's a "horizontal" fling and the ListView should ignore it and allow a child view to handle it.
Can anyone provide some perspective on how this might be accomplished? I'm assuming I'll going to have to do something clever in the onInterceptTouchEvent method of the ListView sub-class.
try this:
final ListView v = new ListView(this);
ArrayAdapter<String> a = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
a.add("one");
a.add("two");
a.add("three");
a.add("four");
v.setAdapter(a);
OnGestureListener ogl = new SimpleOnGestureListener() {
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (Math.abs(velocityX) > Math.abs(velocityY)) {
Log.d(TAG, "onFling " + v.pointToPosition((int) e1.getX(), (int) e1.getY()));
return true;
}
return false;
}
};
final GestureDetector detector = new GestureDetector(ogl);
OnTouchListener otl = new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return detector.onTouchEvent(event);
}
};
v.setOnTouchListener(otl);
OnItemClickListener oicl = new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Log.d(TAG, "onItemClick " + position);
}
};
v.setOnItemClickListener(oicl);
setContentView(v);
I have a listview that displays entries for a given date. There are buttons above the listview that allow you to increase/decrease the date. Everything works. What I'm looking to do is replace those buttons and have the user swipe right/left to increase/decrease the date.
What's the best way to go about this? I don't care what item is swiped, often there will be no items in the listview for a given date, just as long as it happens on the listview area. I do have click and longclick listeners on the items already.
Just implement the OnGestureListener.
public class MyListActivity extends ListActivity implements OnGestureListener
Use a GestureDetector
GestureDetector detector = new GestureDetector(this, this);
Pass the touch event of the list to the GestureDetector
listView.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View view, MotionEvent e) {
detector.onTouchEvent(e);
return false;
}
});
And finally use the fling method to detect a gesture. You can use the velocity values to detect the direction of the movement.
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {}