views:

1628

answers:

8

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?

A: 

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.

Rob Kent
Similar question for a ListView: http://stackoverflow.com/questions/1768391/how-to-detect-android-listview-scrolling-stopped
Rob Kent
Thanks for the suggestion. I've added "implements OnTouchListener" to the definition of the View class, so the whole line reads 'public class MyView extends android.view.View implements OnTouchListener {'. However, the onTouch() method never seems to be called when I tap or scroll. Does the fact that I have a separate GestureDetector class which 'extends SimpleOnGestureListener' mean that the OnTouchListener gets by-passed?
Rob Kent, I've only just seen your comment. So far as I can see the OnScrollListener is only available for a ListView. I'm working with a drawing Canvas on a View that 'extends android.view.View'. Do you know if there's a way I can use an OnScroll method with this (other than the GestureListener I'm already using)?
A: 

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

Bob
Yes, I've tried checking getAction() but it always returns 2 when onScroll is called. The only "up" event that is recognised by onGestureListener is onSingleTapUp; this is not returned at the end of a Scroll.
But in this code they listen for ACTION_UP within onScroll of a SimpleOnGestureListener: http://code.google.com/p/connectbot/source/browse/trunk/connectbot/src/org/connectbot/Console.java?r=54
Bob
Thanks, Bob. This is a mystery. I've inserted `Log.i("onScroll", Integer.toString(e2.getAction()));` as the first statement in onScroll and LogCat just shows a string of "2"'s when I scroll. If I put the Log statement after `if(e2.getAction() == MotionEvent.ACTION_UP)` nothing is logged in LogCat even when I end the scroll.
Sorry, just trying to eliminate possibilities here. See my edit above
Bob
Thanks again for your suggestions. Unfortunately, e1.getAction() returns only 0.
Alright, with those last two suggestions I'm out of ideas. Sorry I can't be more helpful
Bob
My onScroll code returns 'false'. I've tried changing this to 'true' but e2.getAction() still always returns 2 and e1.getAction() still returns 0. How might returning 'true' help? The Android reference says that 'true' should be returned if the event is consumed, so doesn't 'true' prevent any further action in response to the touch/scroll? I'll have a look at your onDown suggestion ...
I've added code for onDown(), but all I see in LogCat is a single 0 from onDown followed by 2's from onScroll, so I can't see how this could help, I'm afraid. Thanks very much for all your suggestions.
+2  A: 

You should take a look at 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.

Layne
Thanks for your suggestions. The Scroller looks as if it is designed to start a scroll in response to a Fling gesture and to continue scrolling for a distance determined by the programmer (somewhat like the Home screen on HTC phones). However, I want the screen image to move with the user's touch. I have followed ideas in the anddev thread in my app. However, so far as I can see this example only draws the buffer image once. My app has an image of potentially unlimited size which needs to be redrawn when the app is idle (after one scroll has finished and before another starts.)
A: 

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

b0x0rz
Thanks for the suggestion. However, I don't think it will help me. I don't want to interrupt the redrawing because I want to scroll smoothly. My approach to this is to do the detailed drawing to a buffer bitmap then in the onDraw() method to plot this bitmap onto the View's canvas. During the course of the Scroll I replot the bitmap shifted slightly to reflect the scroll. What I want to do is detect when the Scroll has ended so that I can redraw the buffer bitmap (which is time-consuming) ready for the next scroll.
A: 

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.

Blue
Thanks for this. Are you suggesting that my code should call the onScroll method and check what it returns. If so, I'm not sure how to do this. I thought that it was a method called by the system to run code inserted by me. I was puzzled by the word "returns" in the documentation, but assumed that they meant to say "return" ... in other words my code should return true at the end of the method if the event had been consumed and false otherwise. Can you clarify for me, please?
Use the return from the touch event as a cue for when the finger has been lifted! When the function returns true the event has finished. So using it in an: If(onScroll == True) { DoThis(); } Else { EffectErrorHandler();} and viola!
Blue
Thanks for your further comment, Blue. I'm afraid I still don't understand what you mean. The onScroll() method is an over-ridden method of OnSimpleGestureListener (it is a method of a class which "extends" SimpleOnGestureListener) and is called by the Android system when a finger is dragged across the screen. How do I call it? Where in my code should I put the call? What arguments should I provide?
The Android Reference for SimpleOnGestureListener is confusing: the section for OnScroll says that the method "returnstrue if the event is consumed, else false" but the Class Overview says the class "does nothing and return false for all applicable methods".
A: 

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

}

Although this worked OK the answer provided by Akos Cz on 29-09-2010 (see above) is much neater. Not only is the code much simpler, but it also avoids any unnecessary delay waiting for a time check. With Akos's code the end of the scroll is detected immediately.
+3  A: 

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);
}
Akos Cz
Thanks for your help. Judging by the thread on Google Code Android, which your link refers to, there seems to be some confusion about the consequences of returning "true" or "false". I'll try "return true" in the onDown method and shall report back.
Thank you very much indeed for this code. It seems to be the answer to my problem. I thought at first the crucial line in your code was "return true" in the onDown method, but in fact this seems to make no difference in my code. The key point seems to be that, so far as I can see, the GestureDetector is never notified of an ACTION_UP event, so as (you correctly identified) I have to include code in my onTouchListener.onTouch() method to look out for this event at the end of the scroll. Thanks again!
A: 
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.

Aron Cederholm
Thanks for this suggestion. I'm afraid I won't have time to try it out for a few days, but I look forward to seeing if it works for me. It will be very neat if it does!
Sadly, onFling only seems to be called at the end of a quick gesture. I need to redraw, not only after a fling, but at the end of a slow precise scroll as well.