Joost
Joost

Reputation: 1476

How to get the scroll speed on a ListView?

I have a ListView with onScrollStateChanged and onScroll event listeners. I want to be able to get the scroll speed of the ListView or some way to get the finalX location of the initiated scroll in some Event listener. Our app targets SDK version 7.

I need to measure or get the speed at which the ListView is scrolling.

Upvotes: 22

Views: 15080

Answers (6)

Dalton Tan
Dalton Tan

Reputation: 485

Here's the code if you need to know how many pixels per second it's scrolling when the items have different sizes or when you have a very long list. Calculating and caching each item size would not work if some items are missed. BTW if you choose that method, you should cache the item offset instead of the height so you don't have to do so many calculation.

Override OnScrollListener:

private HashMap<Integer, Integer> offsetMap = new HashMap<>();
private long prevScrollTime = System.currentTimeMillis();

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        float traveled = 0;
        Set<Integer> oldKeys = new HashSet<>(offsetMap.keySet());
        for (int i = 0; i < visibleItemCount; i++) {
            int pos = firstVisibleItem + i;
            int newOffset = view.getChildAt(i).getTop();
            if (offsetMap.containsKey(pos) && traveled == 0) {
                traveled = Math.abs(newOffset - offsetMap.get(pos));
            }
            offsetMap.put(pos, newOffset);
            oldKeys.remove(pos);
        }
        // remove those that are no longer in view
        for (Integer key : oldKeys) {
            offsetMap.remove(key);
        }
        long newTime = System.currentTimeMillis();
        long t = newTime - prevScrollTime;
        if (t > 0) {
            float speed = traveled / t * 1000f;
        } else {
            // speed = 0
        }
        prevScrollTime = newTime;
}

Upvotes: 0

Autobots
Autobots

Reputation: 1274

There is a simple way to get the speed (NOT Velocity) of the ListView, but it's not the perfect way.

/** The scroll speed threshold, it's a empiric value. */
private static final int LOW_SPEED_SCROLL_THRESHOLD = 3000;
/** The offset, in pixels, by which the content of this view is scrolled vertically. */
private long mScrollY = 0;
/** The last offset, in pixels, by which the content of this view is scrolled vertically. */
private long mLastScrollY = 0;
/** The last scroll time, in millisecond */
private long mLastTime = 0;

public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    final View currentItemView = absListView.getChildAt(0);
    if (currentItemView != null) {
        // The first try to scroll, reset the last time flag
        if (mLastTime == 0) {
            mLastTime = System.currentTimeMillis();
            return;
        }


        final int height = currentItemView.getHeight();
        final int currentPos = firstVisibleItem;
        final int currentPosOffset = currentItemView.getTop(); // < 0
        mScrollY = currentPos * height - currentPosOffset;
        final long deltaOffset = mScrollY - mLastScrollY;
        final long currTime = System.currentTimeMillis();
        if (currTime == mLastTime) {
            return;
        }

        final long speed = deltaOffset * 1000 / (currTime - mLastTime);
        if (Math.abs(speed) < LOW_SPEED_SCROLL_THRESHOLD) {
            // low speed
        } else {
            // high speed
        }

        mLastTime = currTime;
        mLastScrollY = mScrollY;
    } else {
        resetScrollState();
    }
}

private void resetScrollState() {
    mLastTime = 0;
    mScrollY = 0;
    mLastScrollY = 0;
}
  1. The perfect way is to use current scroll Velocity when callback onScroll,in AbsListView, we can get the velocity with FlingRunnable#mScroller.getCurrVelocity(), but unfortunately,the AbsListView doesn't provide the public method of getCurrVelocity(), so if we want to get this method, there are two methods to get it
    • Reflect this method, but I think it has the performance problem when get it onScroll callback
    • Copy the AbsListView.java source and create a new class of AbsListViewEx, provide a public method of getCurrVelocity() in this class, let new ListViewEx extends from AbsListViewEx, but it also has some problem: 1) it maybe a complex thing 2) the ListViewEx may has compatibility problem. However, this way I think is better than Reflect method.

Upvotes: 1

rharter
rharter

Reputation: 2495

So this is just some theoretical pseudo-code, but wouldn't something like this work?

I think the accepted answer doesn't really work the resolution is super low (i.e. you only get a velocity after an item has been scrolled off the entire screen, but what if you have large item views?).

   int mTrackingPosition = -1;
   int mLastTop;
   long mLastEventTime;

   @Override
   public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

       // Get a new tracking position if our old one is no longer on screen
       // (this also includes the first time)
       if (first > mTrackingPosition || last < mTrackingPosition) {

           // We get the middle position here since that's likely to stay 
           // on screen for a bit when scrolling up or down.
           mTrackingPosition = firstVisibleItem + visibleItemCount / 2;

           // Reset our times since we can't really get velocity from this
           // one measurement
           mLastTop = mLastEventTime = -1;

           // Handle the case that this happens more than once in a
           // row if that's even reasonably possible (i.e. they 
           // scrolled rediculously fast)
       }

       // Get the measurements of the tracking view
       View v = view.getViewForPosition(mTrackingPosition);
       int top = v.getTop();
       long time = System.currentTimeMillis();

       // We can only get speed if we have a last recorded time
       if (mLastTop != -1 && mLastEventTime != -1) {
           // Velocity = distance / time
           float velocity = (mLastTop - top) / (time - mLastEventTime);

           // What do you want to do with the velocity?
           ...
       }

       // Update the last top and time so that we can track the difference
       mLastTop = top;
       mLastEventTime = time;
   }

Again this is just pseudo-code that I haven't tested, but I think it should work. You'll also have to reset the last time and top position values when the scroll state is STATE_IDLE.

Upvotes: 2

SSemashko
SSemashko

Reputation: 1497

Division first visible items difference on time difference is not a good solution. OnScroll listener recieves onScroll event every fixed period of time, so in most cases the result of division will be "0".

So you can try something like this:

private OnScrollListener onScrollListener = new OnScrollListener() {

    private int previousFirstVisibleItem = 0;
    private long previousEventTime = 0;
    private double speed = 0;

    @Override
    public void onScroll(HtcAbsListView view, int firstVisibleItem,
            int visibleItemCount, int totalItemCount) {

        if (previousFirstVisibleItem != firstVisibleItem){
            long currTime = System.currentTimeMillis();
            long timeToScrollOneElement = currTime - previousEventTime;
            speed = ((double)1/timeToScrollOneElement)*1000;

            previousFirstVisibleItem = firstVisibleItem;
            previousEventTime = currTime;

            Log.d("DBG", "Speed: " +speed + " elements/second");
        }

    }

    @Override
    public void onScrollStateChanged(HtcAbsListView view, int scrollState) {
    }
};

Upvotes: 17

Angie
Angie

Reputation: 277

you can use android:fastScrollEnabled="true" in you and don't forget to : YourListView.requestFocusFromTouch(); after yourAdapter.notifyDataSetChanged(); because it (yourlistview) lose the focus in fast speed

Upvotes: -1

QuickNick
QuickNick

Reputation: 1931

Try it:

private class SpeedMeterOnScrollListener implements OnScrollListener {

    private long timeStamp;
    private int lastFirstVisibleItem;

    public SpeedMeterOnScrollListener() {
        timeStamp = System.currentTimeMillis();
        lastFirstVisibleItem = 0;
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        long lastTime = System.currentTimeMillis();
        //calculate speed by firstVisibleItem, lastFirstVisibleItem, timeStamp and lastTime
        timeStamp = lastTime;
        lastFirstVisibleItem = firstVisibleItem;
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
    }
}

Upvotes: 5

Related Questions