2

Is it possible to have 2 ViewPagers that simultaniously scroll together, if I start scrolling on one, the other does the exact same scrolling behaviour. Or should I implement somthing other than a ViewPager.

thank you

TootsieRockNRoll
  • 3,218
  • 2
  • 25
  • 50

4 Answers4

4

The solution that worked best for me was to pass MotionEvent in OnTouchListener between ViewPager instances. Tried fake dragging but it was always laggy and buggy (tried the solution from this thread too - didn't work) - I needed a smooth, parallax-like effect.

So, my advice is to implement a View.OnTouchListener. The MotionEvent has to be scaled to compensate for the difference in width.

public class SyncScrollOnTouchListener implements View.OnTouchListener {

private final View syncedView;

public SyncScrollOnTouchListener(@NonNull View syncedView) {
    this.syncedView = syncedView;
}

@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
    MotionEvent syncEvent = MotionEvent.obtain(motionEvent);
    float width1 = view.getWidth();
    float width2 = syncedView.getWidth();

    //sync motion of two view pagers by simulating a touch event
    //offset by its X position, and scaled by width ratio
    syncEvent.setLocation(syncedView.getX() + motionEvent.getX() * width2 / width1,
            motionEvent.getY());
    syncedView.onTouchEvent(syncEvent);
    return false;
}
}

Then set it to your ViewPager

    sourcePager.setOnTouchListener(new SyncScrollOnTouchListener(targetPager));

Note that this solution will only work if both pagers have the same orientation. If you need it to work for different orientations - adjust syncEvent Y coordinate instead of X.

There is one more issue that we need to take into account - minimum fling speed and distance that can cause just one pager to change page.

It can be easily fixed by adding an OnPageChangeListener to our pager

sourcePager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset,
                                   int positionOffsetPixels) {
            //no-op
        }

        @Override
        public void onPageSelected(int position) {
            targetPager.setCurrentItem(position, true);
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            //no-op
        }
    }); 
LukeJanyga
  • 1,115
  • 9
  • 16
3

You can give each an OnPageChangeListsner and implement onPageScrolled (and maybe also onPageSelected for when you change pages without scrolling). Since the logic will be the same, we can write a class for this:

public class ViewPagerScrollSync implements ViewPager.OnPageChangeListener {
    private ViewPager actor; // the one being scrolled
    private ViewPager target; // the one that needs to be scrolled in sync

    public ViewPagerScrollSync(ViewPager actor, ViewPager target) {
        this.actor = actor;
        this.target = target;
        actor.setOnPageChangeListener(this);
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        if (actor.isFakeDragging()) {
            return;
        }

        if (state == ViewPager.SCROLL_STATE_DRAGGING) {
            // actor has begun a drag
            target.beginFakeDrag();
        } else if (state == ViewPager.SCROLL_STATE_IDLE) {
            // actor has finished settling
            target.endFakeDrag();
        }
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (actor.isFakeDragging()) {
            return;
        }
        if (target.isFakeDragging()) {
            // calculate drag amount in pixels.
            // i don't have code for this off the top of my head, but you'll probably
            // have to store the last position and offset from the previous call to
            // this method and take the difference.
            float dx = ...
            target.fakeDragBy(dx);
        }
    }

    @Override
    public void onPageSelected(int position) {
        if (actor.isFakeDragging()) {
            return;
        }

        // Check isFakeDragging here because this callback also occurs when
        // the user lifts his finger on a drag. If it was a real drag, we will
        // have begun a fake drag of the target; otherwise it was probably a
        // programmatic change of the current page.
        if (!target.isFakeDragging()) {
            target.setCurrentItem(position);
        }
    }
}

Then in your Activity/Fragment, you would do this:

ViewPager pager1 = ...
ViewPager pager2 = ...
ViewPagerScrollSync sync1 = new ViewPagerScrollSync(pager1, pager2);
ViewPagerScrollSync sync2 = new ViewPagerScrollSync(pager2, pager1);
Karakuri
  • 38,365
  • 12
  • 84
  • 104
  • I've added some checks for `actor.isFakeDragging()` to prevent cyclical callbacks. You should try it out and see if they're necessary or not. – Karakuri Aug 08 '14 at 15:35
  • Did you guys really succeed using this? I've tried it, but it keeps getting out of sync between the viewPagers. – android developer Jun 30 '15 at 12:49
  • Nice, elegant solution. Two things to add: one is that I got this working when using `target.isFakeDragging` instead of `!target.isFakeDragging` in `onPageScrolled`; the other is that the `onPageScrolled` method seems to get called while the fake drag is still in place for the `target`. This causes the `target` to switch the right position, but then switch back to the position it started from (if "forward scrolling") as soon as the drag is ended. – lucian.pantelimon Feb 18 '16 at 16:10
  • In order to circumvent the last issue, I've added an `endFakeDrag` and `setCurrentItem` for the `actor`, in `onPageSelected` when the actor is fake dragging and added a `target.isFakeDragging` check in `onPageScrollStateChanged` so that `endFakeDrag` isn't called when not fake dragging. – lucian.pantelimon Feb 18 '16 at 16:12
1

you will need to keep an eye on your positions to avoid IndexOutOfBounds errors. Generally:

        viewPager1.setOnPageChangeListener(new OnPageChangeListener() {

        @Override
        public void onPageSelected(int position) {
            viewPager2.setCurrentItem(position);

        }

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            // TODO Auto-generated method stub

        }

        @Override
        public void onPageScrollStateChanged(int state) {
            // TODO Auto-generated method stub

        }
    });

This will work assuming that both ViewPagers have the same number of items. Otherwise, you will need to track your positions to synchronize the behavior of both pagers.

To clarify: if your Adapters have a different number of items or you don't want both pagers to behave exactly the same, you will need to check the position of viewPager1 in the onPageSelected() method and then adjust the position to pass to the setCurrentItem() method of viewPager2

Droidman
  • 11,485
  • 17
  • 93
  • 141
  • wouldn't this only triger when a page is selected, I want something like when I start scrolling, the other view starts also, so it's kinda looks like a mirroring effect – TootsieRockNRoll Aug 08 '14 at 15:26
  • this is all possible with the `OnPageChangeListener`. Please consult the docs regarding this http://developer.android.com/reference/android/support/v4/view/ViewPager.OnPageChangeListener.html – Droidman Aug 08 '14 at 15:27
0

Drop this class in your project

import androidx.viewpager.widget.ViewPager;
/**
 * Sync Scroll 2 View Pager
 */
public class SyncScrollOnTouchListener implements ViewPager.OnPageChangeListener {

    private ViewPager master;
    private ViewPager slave;
    private int mScrollState = ViewPager.SCROLL_STATE_IDLE;

    public SyncScrollOnTouchListener(ViewPager master, ViewPager slave){
        this.master = master;
        this.slave = slave;
    }

    @Override
    public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
        if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
            return;
        }
        this.slave.scrollTo(this.master.getScrollX()*
                this.slave.getWidth()/
                this.master.getWidth(), 0);
    }

    @Override
    public void onPageSelected(final int position) {

    }

    @Override
    public void onPageScrollStateChanged(final int state) {
        mScrollState = state;
        if (state == ViewPager.SCROLL_STATE_IDLE) {
            this.slave.setCurrentItem(this.master
                    .getCurrentItem(), false);
        }
    }
}

Usage:

Now you can sync page change of View Pages

masterPager.addOnPageChangeListener(newSyncScrollOnTouchListener(masterPager,slavePager));

    //Sync Scroll of Pages
  
    leftPanelPager.addOnPageChangeListener(new SyncScrollOnTouchListener(leftPanelPager,rightPanelPager));
    rightPanelPager.addOnPageChangeListener(new SyncScrollOnTouchListener(rightPanelPager,leftPanelPager));
Hitesh Sahu
  • 41,955
  • 17
  • 205
  • 154