Android: How to detect when a scroll has ended - android

I am using the onScroll method of GestureDetector.SimpleOnGestureListener to scroll a large bitmap on a canvas. When the scroll has ended I want to redraw the bitmap in case the user wants to scroll further ... off the edge of the bitmap, but I can't see how to detect when the scroll has ended (the user has lifted his finger from the screen).
e2.getAction() always seems to return the value 2 so that is no help.
e2.getPressure seems to return fairly constant values (around 0.25) until the final onScroll call when the pressure seems to fall to about 0.13. I suppose I could detect this reduction in pressure, but this will be far from foolproof.
There must be a better way: can anyone help, please?

Here is how I solved the problem. Hope this helps.
// declare class member variables
private GestureDetector mGestureDetector;
private OnTouchListener mGestureListener;
private boolean mIsScrolling = false;
public void initGestureDetection() {
// Gesture detection
mGestureDetector = new GestureDetector(new SimpleOnGestureListener() {
#Override
public boolean onDoubleTap(MotionEvent e) {
handleDoubleTap(e);
return true;
}
#Override
public boolean onSingleTapConfirmed(MotionEvent e) {
handleSingleTap(e);
return true;
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// i'm only scrolling along the X axis
mIsScrolling = true;
handleScroll(Math.round((e2.getX() - e1.getX())));
return true;
}
#Override
/**
* Don't know why but we need to intercept this guy and return true so that the other gestures are handled.
* https://code.google.com/p/android/issues/detail?id=8233
*/
public boolean onDown(MotionEvent e) {
Log.d("GestureDetector --> onDown");
return true;
}
});
mGestureListener = new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
if (mGestureDetector.onTouchEvent(event)) {
return true;
}
if(event.getAction() == MotionEvent.ACTION_UP) {
if(mIsScrolling ) {
Log.d("OnTouchListener --> onTouch ACTION_UP");
mIsScrolling = false;
handleScrollFinished();
};
}
return false;
}
};
// attach the OnTouchListener to the image view
mImageView.setOnTouchListener(mGestureListener);
}

You should take a look at http://developer.android.com/reference/android/widget/Scroller.html.
Especially this could be of help (sorted by relevance):
isFinished();
computeScrollOffset();
getFinalY(); getFinalX(); and getCurrY() getCurrX()
getDuration()
This implies that you have to create a Scroller.
If you want to use touching you could also use GestureDetector and define your own canvas scrolling. The following sample is creating a ScrollableImageView and in order to use it, you have to define the measurements of your image. You can define your own scrolling range and after finishing your scrolling the image gets redrawn.
http://www.anddev.org/viewtopic.php?p=31487#31487
Depending on your code you should consider invalidating (int l, int t, int r, int b); for the invalidation.

SimpleOnGestureListener.onFling()
It seems to take place when a scroll ends (i.e. the user lets the finger go), that's what I am using and it works great for me.

Coming back to this after a few months I've now followed a different tack: using a Handler (as in the Android Snake sample) to send a message to the app every 125 milliseconds which prompts it to check whether a Scroll has been started and whether more than 100 milliseconds has elapsed since the last scroll event.
This seems to work pretty well, but if anyone can see any drawbacks or possible improvements I should be grateful to hear of them.
The relevant the code is in the MyView class:
public class MyView extends android.view.View {
...
private long timeCheckInterval = 125; // milliseconds
private long scrollEndInterval = 100;
public long latestScrollEventTime;
public boolean scrollInProgress = false;
public MyView(Context context) {
super(context);
}
private timeCheckHandler mTimeCheckHandler = new timeCheckHandler();
class timeCheckHandler extends Handler{
#Override
public void handleMessage(Message msg) {
long now = System.currentTimeMillis();
if (scrollInProgress && (now>latestScrollEventTime+scrollEndInterval)) {
scrollInProgress = false;
// Scroll has ended, so insert code here
// which calls doDrawing() method
// to redraw bitmap re-centred where scroll ended
[ layout or view ].invalidate();
}
this.sleep(timeCheckInterval);
}
public void sleep(long delayMillis) {
this.removeMessages(0);
sendMessageDelayed(obtainMessage(0), delayMillis);
}
}
}
#Override protected void onDraw(Canvas canvas){
super.onDraw(canvas);
// code to draw large buffer bitmap onto the view's canvas
// positioned to take account of any scroll that is in progress
}
public void doDrawing() {
// code to do detailed (and time-consuming) drawing
// onto large buffer bitmap
// the following instruction resets the Time Check clock
// the clock is first started when
// the main activity calls this method when the app starts
mTimeCheckHandler.sleep(timeCheckInterval);
}
// rest of MyView class
}
and in the MyGestureDetector class
public class MyGestureDetector extends SimpleOnGestureListener {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
[MyView].scrollInProgress = true;
long now = System.currentTimeMillis();
[MyView].latestScrollEventTime =now;
[MyView].scrollX += (int) distanceX;
[MyView].scrollY += (int) distanceY;
// the next instruction causes the View's onDraw method to be called
// which plots the buffer bitmap onto the screen
// shifted to take account of the scroll
[MyView].invalidate();
}
// rest of MyGestureDetector class
}

This is what worked for me.
I've enriched the existing GestureDetector.OnGestureListener with onFingerUp() method. This listener does everything as the built-in GestureDetector and it can also listen to the finger up event (it's not onFling() as this is called only when the finger is lifted up along with a quick swipe action).
import android.content.Context;
import android.os.Handler;
import android.view.GestureDetector;
import android.view.MotionEvent;
public class FingerUpGestureDetector extends GestureDetector {
FingerUpGestureDetector.OnGestureListener fListener;
public FingerUpGestureDetector(Context context, OnGestureListener listener) {
super(context, listener);
fListener = listener;
}
public FingerUpGestureDetector(Context context, GestureDetector.OnGestureListener listener, OnGestureListener fListener) {
super(context, listener);
this.fListener = fListener;
}
public FingerUpGestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler, OnGestureListener fListener) {
super(context, listener, handler);
this.fListener = fListener;
}
public FingerUpGestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler, boolean unused, OnGestureListener fListener) {
super(context, listener, handler, unused);
this.fListener = fListener;
}
public interface OnGestureListener extends GestureDetector.OnGestureListener {
boolean onFingerUp(MotionEvent e);
}
public static class SimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements FingerUpGestureDetector.OnGestureListener {
#Override
public boolean onFingerUp(MotionEvent e) {
return false;
}
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
if (super.onTouchEvent(ev)) return true;
if (ev.getAction() == MotionEvent.ACTION_UP) {
return fListener.onFingerUp(ev);
}
return false;
}
}

I think this will work as you need
protected class SnappingGestureDetectorListener extends SimpleOnGestureListener{
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY){
boolean result = super.onScroll(e1, e2, distanceX, distanceY);
if(!result){
//Do what you need to do when the scrolling stop here
}
return result;
}
}

I am sure it is too late for you, however, it seems I have found the right solution to your original question and not necessarily the intention.
If you are using Scroller/OverScroller Object for scrolling you should check the return value from the following function.
public boolean computeScrollOffset()

I was looking into this same issue. I saw Akos Cz's answer to your question. I created something similar, but with my version, I noticed that it only worked for a regular scroll - meaning one that doesn't generate a fling. But if a fling did get generated - regardless if I processed a fling or not, then it did NOT detect the "ACTION_UP" in "onTouchEvent". Now maybe this was just something with my implementation, but if it was I couldn't figure out why.
After further investigation, I noticed that during a fling, the "ACTION_UP" was passed into "onFling" in "e2" every time. So I figured that must be why it wasn't being handled in "onTouchEvent" in those instances.
To make it work for me I only had to call a method to handle the "ACTION_UP" in "onFling" and then it worked for both types of scrolling. Below are the exact steps I took to implement my app:
-initialized a "gestureScrolling" boolean to "false" in a constructor.
-I set it to "true" in "onScroll"
-created a method to handle the "ACTION_UP" event. Inside that event, I reset "gestureSCrolling" to false and then did the rest of the processing I needed to do.
-in "onTouchEvent", if an "ACTION_UP" was detected and "gestureScrolling" = true, then I called my method to handle "ACTION_UP"
-And the part that I did that was different was: I also called my method to handle "ACTION_UP" inside of "onFling".

I haven't done this myself but looking at onTouch() you always get a sequence 0<2>1, so the end has to be a 1 for finger lift.

I don't know Android, but looking at the documentation it seems Rob is right: Android ACTION_UP constant Try checking for ACTION_UP from getAction()?
Edit: What does e1.getAction() show? Does it ever return ACTION_UP? The documentation says it holds the initial down event, so maybe it'll also notify when the pointer is up
Edit: Only two more things I can think of. Are you returning false at any point? That may prevent ACTION_UP
The only other thing I'd try is to have a seperate event, maybe onDown, and set a flag within onScroll such as isScrolling. When ACTION_UP is given to onDown and isScrolling is set then you could do whatever you want and reset isScrolling to false. That is, assuming onDown gets called along with onScroll, and getAction will return ACTION_UP during onDown

i have not tried / used this but an idea for an approach:
stop / interrupt redrawing canvas on EVERY scroll event wait 1s and then start redrawing canvas on EVERY scroll.
this will lead to performing the redraw only at scroll end as only the last scroll will actually be uninterrupted for the redraw to complete.
hope this idea helps you :)

Extract from the onScroll event from GestureListener API: link text
public abstract boolean onScroll
(MotionEvent e1, MotionEvent e2, float
distanceX, float distanceY) Since: API
Level 1
Returns
* true if the event is consumed, else false
Perhaps once the event has been consumed, the action is finished and the user has taken their finger off the screen or at the least finished this onScroll action
You can then use this in an IF statement to scan for == true and then commence with the next action.

If you're using SimpleGestureDetector to handle your scroll events, you can do this
fun handleTouchEvents(event: MotionEvent): Boolean {
if(event.action == ACTION_UP) yourListener.onScrollEnd()
return gestureDetector.onTouchEvent(event)
}

My attempt at adding additional functionality to the gesture detector. Hope it helps someone put his time to better use... gist

Related

How to add a gesture detector to a view in Android

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

OnTouchListener on entire Activity

I'm trying to detect a onFling event on the entire area of my activity. To do this I:
1) set the GestureDetectorCompat:
private GestureDetectorCompat gDetect;
2) in OnCreate initialize the detector:
gDetect = new GestureDetectorCompat(this, new GestureListener());
3) override the onTouchEvent:
#Override
public boolean onTouchEvent(MotionEvent event) {
gDetect.onTouchEvent(event);
return super.onTouchEvent(event);
}
4) create the GestureListener class:
class GestureListener extends GestureDetector.SimpleOnGestureListener {
#Override
public boolean onDown(MotionEvent event) {
return true;
}
#Override
public boolean onFling(MotionEvent event1, MotionEvent event2,
float velocityX, float velocityY) {
Log.e(null, "onFling: " + event1.toString() + event2.toString());
return super.onFling(event1, event2, velocityX, velocityY);
}
}
It works, but it detects onFling gesture ONLY in the part of my UI where there is not any other view. So, if I swipe in a blank area it works, if I swipe in a part of the activity where is another view (for example a ScrollView or a LinearLayout) the detection isn't triggered.
I hope I've explained my problem: how I can detect gesture on the entire surface of the Activity, to accomplish a simple swipe gesture?
Many tanks.
You can use GestureOverlayView. Here is a great starter resource for GestureOverlayView . Also you can have a look at this SO question.
Basically to use this your application must use the view "GestureOverlayView" and you place your other views in this view. Your activity must implement the interface "OnGesturePerformedListener" and if a gesture is detected then "onGesturePerformedListener" method is called.
Hope this helps. Goodluck!

Implementing onTouchListener and onGestureLitener simultaneously

I am facing a situation.
I have written a class implementing onTouchListener which I have registered with view(GLSurfaceView, to be precise) and I used it to extract all
information about touch events. And it serves me very well. And implementing onTouch
method means that event is handled then and there and its not propagated down.
But of late I need to implement swipe functionality for my game. And it needs that I implement
onTouchEvent for my view. But as my onTouchListener implementation class is already implementing onTouch method, so onTouchEvent won't be called. And I want to keep my onTouchListener implementation.
Please tell me how to solve this issue.
Manish
In your class put the following:
//not necessarily OnCreate but put it in whatever constructor your class uses
OnCreate(Context context){
mGestureDetector = new GestureDetector(context, new CustomGestureListener());
}
#Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
return super.onTouchEvent(event);
}
private final GestureDetector mGestureDetector;
private class CustomGestureListener extends SimpleOnGestureListener {
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//get the fling
}
}
This allows the onTouchEvent to execute, but it also checks for a gesture event as well by calling the gesture detector within the onTouchEvent.
See also this answer: How to set OnTouchListener for the entire screen?

Android ListView tolerance

I have list items that have a HorizontalScrollView in each of them. Hence, I should be able to swipe up and down the list and also swipe through the list item horizontally. (e.g. Pulse News App).
What I'm encountering is that, whenever I scroll the HorizontalScrollView, the ListView also gets scrolled by tiny bit but giving out annoying disturbances in terms of User Experience.
So, I was actually thinking to subclass ListView and implement my own ListView widget. However, I don't know which method to override so that I could increase X tolerance and stop the listview from giving these unwanted flickering.
gestureDetector = new GestureDetector(new YScrollDetector());
public boolean onInterceptTouchEvent(MotionEvent ev) {
//Call super first because it does some hidden motion event handling
boolean result = super.onInterceptTouchEvent(ev);
//Now see if we are scrolling vertically with the custom gesture detector
if (gestureDetector.onTouchEvent(ev)) {
return result;
}
//If not scrolling vertically (more y than x), don't hijack the event.
else {
return false;
}
}
// Return false if we're scrolling in the x direction
class YScrollDetector extends SimpleOnGestureListener {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
try {
if (Math.abs(distanceY) > Math.abs(distanceX)) {
return true;
} else {
return false;
}
} catch (Exception e) {
// nothing
}
return false;
}
}
This is will help you, you have to override these in the custom list view you create,i used this to eliminate the problem in my application. Hope this helps.

Handling UI Events in Android

I'm trying to modify some code I found online to my needs. It is supposed to catch a MotionEvent (a fling in particular) and launch a separate activity. I am new to java, so I'm having some trouble understanding the code. Here's what I have so far:
public class Hypertension extends Activity {
private GestureDetector flingDetector;
View.OnTouchListener gestureListener;
private TextView redView;
private TextView greenView;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
redView = (TextView)findViewById(R.id.buttonRed);
greenView = (TextView)findViewById(R.id.buttonGreen);
redView.setOnTouchListener(gestureListener);
greenView.setOnTouchListener(gestureListener);
flingDetector = new GestureDetector(new MyFlingListener());
gestureListener = new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
if (flingDetector.onTouchEvent(event)) {
//startActivity(new Intent(Hypertension.this, Green.class));
return true;
}
return false;
}
};
}
class MyFlingListener extends SimpleOnGestureListener {
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
// TODO Auto-generated method stub
startActivity(new Intent(Hypertension.this, Green.class));
return false;
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (flingDetector.onTouchEvent(event))
return true;
else
return false;
}
}
My understanding is that when the screen is touched, onTouchEvent is called. In the above code, this method is overridden to call flingDetector, which listens for a fling. If one is heard, it launches the activity.
As of now, however, nothing happens when I perform a fling in the emulator. Also, I am fairly confused as to what the return values of the various boolean methods actually represent.
You have two onTouchEvent methods in your code. One is in the GestureDetector class (not overridden), and the other is in your Hypertension activity class (which you have overridden at the bottom).
When someone triggers the TouchEvent in the main activity you explicitly calling the GestureDetector one (passing down the event) here:
if (flingDetector.onTouchEvent(event)) return true;
But if you haven't overridden the onTouchEvent method of that class then nothing is going to happen!
The purpose of overriding these "onSomething()" methods is so that they will get called automatically when an event triggers. In general the way to work with listeners is as follows:
Create a subclass of the Listener class for the event and override its "onEvent()" method to do something when the event is triggered
Call the "setListener( Listener)" method of the object you want to trigger said events after it has been initialized--passing in your previously created Listener
Sit back and watch :)
For all event listeners that return a boolean it should return true when it handles the event, so in your example if the flingDetector handles the onTouchEvent it returns true.
This question has been posed before and there are some great answers there.

Categories

Resources