Android: How to detect when a scroll has ended

AndroidAndroid CanvasSmooth Scrolling

Android Problem Overview


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?

Android Solutions


Solution 1 - Android

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

Solution 2 - Android

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 invalidate(int l, int t, int r, int b); for the invalidation.

Solution 3 - Android

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.

Solution 4 - Android

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

}

Solution 5 - Android

I was looking into this same issue. I saw Akos Cz 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 in 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".

Solution 6 - Android

I am sure it is too late for you, however, it seems I have found the right solution to your original question and not necessary the intention.

If you are using Scroller/OverScroller Object for scrolling you should check the return value from the following function.

public boolean computeScrollOffset() 

enjoy

harvinder

Solution 7 - Android

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

Solution 8 - Android

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

Solution 9 - Android

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.

Solution 10 - Android

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

Solution 11 - Android

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 :)

Solution 12 - Android

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.

Solution 13 - Android

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

https://gist.github.com/WildOrangutan/043807ddbd68978179b7cea3b53c71e8

Solution 14 - Android

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionprepbggView Question on Stackoverflow
Solution 1 - AndroidAkos CzView Answer on Stackoverflow
Solution 2 - Androiduser238801View Answer on Stackoverflow
Solution 3 - AndroidAron CederholmView Answer on Stackoverflow
Solution 4 - AndroidprepbggView Answer on Stackoverflow
Solution 5 - AndroidAlexView Answer on Stackoverflow
Solution 6 - Androidresp78View Answer on Stackoverflow
Solution 7 - AndroidnautilusvnView Answer on Stackoverflow
Solution 8 - AndroidpepanView Answer on Stackoverflow
Solution 9 - AndroidRob KentView Answer on Stackoverflow
Solution 10 - AndroidBobView Answer on Stackoverflow
Solution 11 - Androidb0x0rzView Answer on Stackoverflow
Solution 12 - AndroidTom 'Blue' PiddockView Answer on Stackoverflow
Solution 13 - AndroidPeterView Answer on Stackoverflow
Solution 14 - AndroidNicolas DuponchelView Answer on Stackoverflow