Android: Velocity-Based Viewpager Scrolling

Android: velocity-based ViewPager scrolling

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.

Android ViewPager scrolling issue with only one item when using getPageWidth from PagerAdapter

I know this is an old question, but I was just looking for a solution and came across this link (which oddly references this question). Anyway, I was able to figure out a solution based on their comments. The basic idea is to allow touch events based on the state of a boolean flag which you set.

  1. Extend ViewPager on override onInterceptTouchEvent & onTouchEvent to only call super if you've set the flag. My class looks like this:

    public class MyViewPager extends ViewPager {

    private boolean isPagingEnabled = false;

    public MyViewPager(Context context, AttributeSet attrs) {
    super(context, attrs);
    }

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
    if (isPagingEnabled) {
    return super.onInterceptTouchEvent(event);
    }
    return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    if (isPagingEnabled) {
    return super.onTouchEvent(event);
    }
    return false;
    }

    public void setPagingEnabled(boolean pagingEnabled) {
    isPagingEnabled = pagingEnabled;
    }
    }
  2. In your layout xml, replace your com.android.support.v4.ViewPager elements with com.yourpackage.MyViewPager elements.

  3. Since you return 0.3f from getPageWidth() in your pager adapter, you would want scrolling enabled when the fourth item has been added to it. The tricky part is having this line of code everywhere when you define your pager adapter, and add or remove any objects from the adapter backing list.

    mPager.setPagingEnabled(items.size() > 3);

Slowing speed of Viewpager controller in android

I've started with HighFlyer's code which indeed changed the mScroller field (which is a great start) but didn't help extend the duration of the scroll because ViewPager explicitly passes the duration to the mScroller when requesting to scroll.

Extending ViewPager didn't work as the important method (smoothScrollTo) can't be overridden.

I ended up fixing this by extending Scroller with this code:

public class FixedSpeedScroller extends Scroller {

private int mDuration = 5000;

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

public FixedSpeedScroller(Context context, Interpolator interpolator) {
super(context, interpolator);
}

public FixedSpeedScroller(Context context, Interpolator interpolator, boolean flywheel) {
super(context, interpolator, flywheel);
}


@Override
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
// Ignore received duration, use fixed one instead
super.startScroll(startX, startY, dx, dy, mDuration);
}

@Override
public void startScroll(int startX, int startY, int dx, int dy) {
// Ignore received duration, use fixed one instead
super.startScroll(startX, startY, dx, dy, mDuration);
}
}

And using it like this:

try {
Field mScroller;
mScroller = ViewPager.class.getDeclaredField("mScroller");
mScroller.setAccessible(true);
FixedSpeedScroller scroller = new FixedSpeedScroller(mPager.getContext(), sInterpolator);
// scroller.setFixedDuration(5000);
mScroller.set(mPager, scroller);
} catch (NoSuchFieldException e) {
} catch (IllegalArgumentException e) {
} catch (IllegalAccessException e) {
}

I've basically hardcoded the duration to 5 seconds and made my ViewPager use it.

Scroll more than one item in ViewPager

ViewPager is not the answer. You need HorizontalScrollView with paging. that enables you when flinging to swipe multiple pages but doesnt stop in the middle of a page.

In case u still want ViewPager -
Read this

horizontalscrollview inside viewpager

In the end I made a custom pager and overrided the method canScroll like so

@Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof HorizontalScrollView) {
HorizontalScrollView scroll = (HorizontalScrollView) v;

int vScrollX = scroll.getScrollX();
TableLayout table = (TableLayout) scroll.getChildAt(scroll
.getChildCount() - 1);
int diff = (table.getRight() - (scroll.getWidth()
+ scroll.getScrollX() + table.getLeft()));

if (vScrollX == 0 && diff <= 0) {// table without scroll
if (dx > 20 && this.getCurrentItem() > 0) {
this.setCurrentItem(this.getCurrentItem() - 1, true);
} else if (dx < -20
&& this.getCurrentItem() + 1 < this.getChildCount()) {
this.setCurrentItem(this.getCurrentItem() + 1, true);
}
return false; // change page
}
if (vScrollX == 0 && dx > 20) {// left edge, swiping right
if (this.getCurrentItem() > 0) {
this.setCurrentItem(this.getCurrentItem() - 1, true);
}
return false; // change page
}
if (vScrollX == 0 && dx < -20) {// left edge, swiping left
return true;// scroll
}
if (diff <= 0 && dx > 20) {// right edge, swiping right
return true;// scroll
}
if (diff <= 0 && dx < -20) {// right edge, swiping left
if (this.getCurrentItem() + 1 < this.getChildCount()) {
this.setCurrentItem(this.getCurrentItem() + 1, true);
}
return false;// change page
}
}
return super.canScroll(v, checkV, dx, x, y);
}

Is there a way to customize the threshold for ViewPager scrolling?

Going out on a bit of a limb here; I have no doubt that you may have to tweak this concept and/or code.

I will recommend what I did in the comments above; to extend ViewPager and override its public constructors, and call a custom method which is a clone of initViewPager() (seen in the source you provided). However, as you noted, mFlingDistance, mMinimumVelocity, and mMaximumVelocity are private fields, so they can't be accessed by a subclass.

(Note: You could also change these methods after the constructors are called, if you wanted to.)

Here's where it gets a bit tricky. In Android, we can use the Java Reflection API to make those fields accessible.

It should work something like this:

Class clss = getClass.getSuperClass();
Field flingField = clss.getDeclaredField("mFlingDistance"); // Of course create other variables for the other two fields
flingField.setAccessible(true);
flingField.setInt(this, <whatever int value you want>); // "this" must be the reference to the subclass object

Then, repeat this for the other two variables with whatever values you want. You may want to look at how these are calculated in the source.

Unfortunately, as much as I would like to recommend using Reflection to override the private method determineTargetPage(), I don't believe it's possible to do this--even with the expansive Reflection API.

How to limit ViewPager page changes to one per gesture

I don't know exactly what are you trying to achieve here, but instead of using a negative margin wouldn't be better to user getPageWidth() on the PagerAdapter to make the pages width smaller than the ViewPager width?

Edit

Ok, I came up with a 'hacky' solution, consisting in copying the source code from ViewPager into your project and modifying a little bit performDrag(), so it will not drag more than one page.

This Hack works with some assumptions:

  1. It uses getPageWidth() to determine the relative size of the pages
  2. All pages must return the same getPageWidth() value

You can check the sample project, and the ViewPager modifications diff

It not a beautiful or elegant solution, but it works, and I'll finally close a 3 months old ticket with this same issue, so manager happy :)



Related Topics



Leave a reply



Submit