TootsieRockNRoll
TootsieRockNRoll

Reputation: 3288

Android 2 ViewPagers simultanious scroll

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

Upvotes: 2

Views: 2208

Answers (4)

Hitesh Sahu
Hitesh Sahu

Reputation: 45052

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

Upvotes: 0

LukeJanyga
LukeJanyga

Reputation: 1125

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
        }
    }); 

Upvotes: 4

Karakuri
Karakuri

Reputation: 38585

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

Upvotes: 3

Droidman
Droidman

Reputation: 11608

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

Upvotes: 1

Related Questions