Android: velocity-based ViewPager scrolling
Asked Answered
K

5

45

The way the ViewPager scrolls right now is by one item per gesture. It treats flinging gesture the same way no matter if it's full screen fast fling or slow dragging; at the end page advances one step only.

Is there any projects perhaps or examples that would add velocity-based flinging that scrolls multiple items based on velocity of the existing fling (if it still in progress) and scrolls further if the flinging gesture is wide and fast?

And if there's none where to start with something like this?

P.S. The bounty is offered. Please no answers with references to Gallery or HorizontalScrollView

Kamerun answered 8/10, 2012 at 18:20 Comment(5)
I thought the Gallery behaves like this, have you tried it to see if it suits your needs? developer.android.com/reference/android/widget/Gallery.htmlRishi
Could be, I need to check it outKamerun
And no +Ian Warwick - using Gallery is out of questionKamerun
Do you need to work with ViewPager or you can consider other classes like the ViewFlipper?Discounter
It's already a ViewPager and it suits my needs exactly. All I need is the enhancement to make it scroll as described, so no ViewFlipperKamerun
E
41

The technique here is to extends ViewPager and mimic most of what the pager will be doing internally, coupled with scrolling logic from the Gallery widget. The general idea is to monitor the fling (and velocity and accompanying scrolls) and then feed them in as fake drag events to the underlying ViewPager. If you do this alone, it won't work though (you'll still get only one page scroll). This happens because the fake drag implements caps on the bounds that the scroll will be effective. You can mimic the calculations in the extended ViewPager and detect when this will happen, then just flip the page and continue as usual. The benefit of using fake drag means you don't have to deal with snapping to pages or handling the edges of the ViewPager.

I tested the following code on the animation demos example, downloadable from http://developer.android.com/training/animation/screen-slide.html by replacing the ViewPager in ScreenSlideActivity with this VelocityViewPager (both in the layout activity_screen_slide and the field within the Activity).

/*
 * Copyright 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 * 
 * Author: Dororo @ StackOverflow
 * An extended ViewPager which implements multiple page flinging.
 * 
 */

package com.example.android.animationsdemo;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.GestureDetector;
import android.widget.Scroller;

public class VelocityViewPager extends ViewPager implements GestureDetector.OnGestureListener {

private GestureDetector mGestureDetector;
private FlingRunnable mFlingRunnable = new FlingRunnable();
private boolean mScrolling = false;

public VelocityViewPager(Context context) {
    super(context);
}

public VelocityViewPager(Context context, AttributeSet attrs) {
    super(context, attrs);
    mGestureDetector = new GestureDetector(context, this);
}

// We have to intercept this touch event else fakeDrag functions won't work as it will
// be in a real drag when we want to initialise the fake drag.
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    return true;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    // give all the events to the gesture detector. I'm returning true here so the viewpager doesn't
    // get any events at all, I'm sure you could adjust this to make that not true.
    mGestureDetector.onTouchEvent(event);
    return true;
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velX, float velY) {
    mFlingRunnable.startUsingVelocity((int)velX);
    return false;
}

private void trackMotion(float distX) {

    // The following mimics the underlying calculations in ViewPager
    float scrollX = getScrollX() - distX;
    final int width = getWidth();
    final int widthWithMargin = width + this.getPageMargin();
    final float leftBound = Math.max(0, (this.getCurrentItem() - 1) * widthWithMargin);
    final float rightBound = Math.min(this.getCurrentItem() + 1, this.getAdapter().getCount() - 1) * widthWithMargin;

    if (scrollX < leftBound) {
        scrollX = leftBound;
        // Now we know that we've hit the bound, flip the page
        if (this.getCurrentItem() > 0) {
            this.setCurrentItem(this.getCurrentItem() - 1, false);
        }
    } 
    else if (scrollX > rightBound) {
        scrollX = rightBound;
        // Now we know that we've hit the bound, flip the page
        if (this.getCurrentItem() < (this.getAdapter().getCount() - 1) ) {
            this.setCurrentItem(this.getCurrentItem() + 1, false);
        }
    }

    // Do the fake dragging
    if (mScrolling) {
        this.fakeDragBy(distX);
    }
    else {
        this.beginFakeDrag();
        this.fakeDragBy(distX);
        mScrolling = true;
    }

}

private void endFlingMotion() {
    mScrolling = false;
    this.endFakeDrag();
}

// The fling runnable which moves the view pager and tracks decay
private class FlingRunnable implements Runnable {
    private Scroller mScroller; // use this to store the points which will be used to create the scroll
    private int mLastFlingX;

    private FlingRunnable() {
        mScroller = new Scroller(getContext());
    }

    public void startUsingVelocity(int initialVel) {
        if (initialVel == 0) {
            // there is no velocity to fling!
            return;
        }

        removeCallbacks(this); // stop pending flings

        int initialX = initialVel < 0 ? Integer.MAX_VALUE : 0;
        mLastFlingX = initialX;
        // setup the scroller to calulate the new x positions based on the initial velocity. Impose no cap on the min/max x values.
        mScroller.fling(initialX, 0, initialVel, 0, 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);

        post(this);
    }

    private void endFling() {
        mScroller.forceFinished(true);
        endFlingMotion();
    }

    @Override
    public void run() {

        final Scroller scroller = mScroller;
        boolean animationNotFinished = scroller.computeScrollOffset();
        final int x = scroller.getCurrX();
        int delta = x - mLastFlingX;

        trackMotion(delta); 

        if (animationNotFinished) {
            mLastFlingX = x;
            post(this);
        }
        else {
            endFling();
        }

    }
}

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distX, float distY) {
    trackMotion(-distX);
    return false;
}

    // Unused Gesture Detector functions below

@Override
public boolean onDown(MotionEvent event) {
    return false;
}

@Override
public void onLongPress(MotionEvent event) {
    // we don't want to do anything on a long press, though you should probably feed this to the page being long-pressed.
}

@Override
public void onShowPress(MotionEvent event) {
    // we don't want to show any visual feedback
}

@Override
public boolean onSingleTapUp(MotionEvent event) {
    // we don't want to snap to the next page on a tap so ignore this
    return false;
}

}

There are a few minor issues with this, which can be resolved easily but I will leave up to you, namely things like if you scroll (dragging, not flinging) you can end up half way between pages (you'll want to snap on the ACTION_UP event). Also, touch events are being completely overridden in order to do this, so you will need to feed relevant events to the underlying ViewPager where appropriate.

Expurgate answered 19/2, 2013 at 23:54 Comment(8)
Thanks @Expurgate - I'll give it a try to see what I'm missing hereKamerun
I tried this one, and it works fine, but if I do a pager.setCurrentItem(m_CurrPage, false) from the code to initialize it to a page different from 0 at startup, fling not work, and everytime it go back to the first page, how can I initialize the start page to make it work?Partook
This solution doesn't work well for me. Touch events seem inconsistent and there's a lot of jumping. Anyone else having issues?Judaica
Thanks for the solution @Expurgate About, but about the minor issues, anybody knows how to solve them? All events are being eaten by the velocityViewPager and I can't figure out on how to pass them back to the main activity. I've tried several things but nothing seems to work.Appease
I've come up with an alternative solution: copy a whole ViewPager implementation from the support library and customize a determineTargetPage(...) method so that it calculates target page of scrolling based on the incoming velocity.Buddhism
@Partook Somehow getScrollX returns 0 sometimes. Did you get to any solution to this?Overtrade
To make this code work with setCurrentItem we need to maintain offset.Add/modify below lines in trackMotion method above -------> float scrollOffset = getScrollX() - (this.getCurrentItem() * widthWithMargin); float scrollX = getScrollX() - distX - scrollOffset;Overtrade
@HimanshuVirmani Unfortunately that change breaks the smooth scrolling on fling. Causes the ViewPager to stop scrolling once it reaches the end of the preloaded pages (based on setOffscreenPageLimit()).Lyautey
B
4

Another option is to copy a whole ViewPager implementation source code from the support library and customize a determineTargetPage(...) method. It's responsible for determining to which page to scroll to on fling gesture. This approach is not super convenient, but works pretty well. See implementation code below:

private int determineTargetPage(int curPage, float pageOffset, int velocity, int dx) {
    int target;
    if (Math.abs(dx) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
        target = calculateFinalPage(curPage, velocity);
    } else {
        final float truncator = curPage >= mCurItem ? 0.4f : 0.6f;
        target = (int) (curPage + pageOffset + truncator);
    }
    if (mItems.size() > 0) {
        final ItemInfo first = mItems.get(0);
        final ItemInfo last = mItems.get(mItems.size() - 1);

        // Only let the user target pages we have items for
        target = Math.max(first.position, Math.min(target, last.position));
    }
    return target;
}

private int calculateFinalPage(int curPage, int velocity) {
    float distance = Math.abs(velocity) * MAX_SETTLE_DURATION / 1000f;
    float normalDistance = (float) Math.sqrt(distance / 2) * 25;
    int step = (int) - Math.signum(velocity);
    int width = getClientWidth();
    int page = curPage;
    for (int i = curPage; i >= 0 && i < mAdapter.getCount(); i += step) {
        float pageWidth = mAdapter.getPageWidth(i);
        float remainingDistance = normalDistance - pageWidth * width;
        if (remainingDistance >= 0) {
            normalDistance = remainingDistance;
        } else {
            page = i;
            break;
        }
    }
    return page;
}
Buddhism answered 12/3, 2014 at 15:10 Comment(3)
Can you give us gist of your version of the viewpager?Enduring
It's like the viewpager...same thing !Eldenelder
I tried what you did but now I can an issue with the mAdapter. It is made to hanlde android.support.v4.view.ViewPager instances, not com.mypackage.ViewPager. How did you solve this issue?Mcgowen
T
2

I've found a better realization for me than in the checked answer, this ViewPager behaves better with touches when I want to stop scrolling https://github.com/Benjamin-Dobell/VelocityViewPager

Trochelminth answered 19/8, 2016 at 10:2 Comment(0)
V
0

ViewPager is class from support library. Download support library source code and change about 10 lines of code in onTouchEvent method to add desired feature.

I use modified support library in my projects for about a year, because sometimes I need to modify several lines of code to make a little change or to add new method and I dont want to copy components source code. I use modified version of fragments and viewpager.

But there is one problem you'll get: once in about 6 mounth you have to merge custom support library with new official version if you need new features. And be careful with changes, you dont want to break support library classes compatibility.

Venatic answered 26/2, 2013 at 10:30 Comment(1)
This really doesn't helpOxy
A
-1

You can override ScrollView or HorizontalScrollView class, and add that behavior. There are many bugs in Gallery, and as I remember it's deprecated since api level 14.

Aphasia answered 16/10, 2012 at 8:39 Comment(3)
I read the question, and there is a comment about Gallery. Your wrote "Could be, I need to check it out" about Gallery, I can't add comment, and thats why added as answer.Aphasia
I referred to Gallery code as a possible example on how to enhance ViewPager scrolling. The question is strictly about ViewPager, I'm over looking at the alternativesKamerun
I wrote something like that extending HorizontalScrollView. I believe HorizontalScrollView is the way you should go.Aphasia

© 2022 - 2024 — McMap. All rights reserved.