How to have a parallax effect between 2 ViewPagers?
Asked Answered
D

4

16

Background

Consider the following scenario:

  1. There are 2 viewPagers, each of different width and height, but both have the exact same number of pages.
  2. dragging on one should drag the other, so the effect is like a "parallax" effect.
  3. same as #2, but for the other viewPager.
  4. there is also an auto-switching mechanism between the viewpagers, so every 2 seconds it will auto-switch to the next page (and if on the last page, switch back to the first).

The problem

I think I got some functionality working, but it has some issues:

  1. This is just simple XML. nothing special here.
  2. I've used 'OnPageChangeListener' and there, in 'onPageScrolled' , I've used fake-dragging. Problem is, if the dragging of the user stops near the middle of the page (thus activates auto-scrolling the viewPager left/right), the fake dragging loses its sync, and weird things can occur: wrong page or even staying between pages...
  3. For now, I don't handle the other viewPager with the dragging of the user, but it might be an issue too.
  4. this can be an issue when I succeed solving #2 (or #3). For now, I'm using a handler that use a runnable and calls this command inside :

    mViewPager.setCurrentItem((mViewPager.getCurrentItem() + 1) % mViewPager.getAdapter().getCount(), true);
    

What I've tried

I've tried this post, but it didn't work well, as it got the same issues when the user-dragging stops before finished swithching to another page.

Only similar solution that works, is with a single ViewPager (here), but I need to work with 2 ViewPagers, each affects the other.

So I've tried doing it myself: suppose one viewPager is "viewPager", and the other (that is supposed to follow "viewPager") is "viewPager2" , this is what I've done:

    viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        int lastPositionOffsetPixels=Integer.MIN_VALUE;

        @Override
        public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
            if (!viewPager2.isFakeDragging())
                viewPager2.beginFakeDrag();
            if(lastPositionOffsetPixels==Integer.MIN_VALUE) {
                lastPositionOffsetPixels=positionOffsetPixels;
                return;
            }
            viewPager2.fakeDragBy((lastPositionOffsetPixels - positionOffsetPixels) * viewPager2.getWidth() / viewPager.getWidth());
            lastPositionOffsetPixels = positionOffsetPixels;
        }

        @Override
        public void onPageSelected(final int position) {
            if (viewPager2.isFakeDragging())
                viewPager2.endFakeDrag();
            viewPager2.setCurrentItem(position,true);
        }

        @Override
        public void onPageScrollStateChanged(final int state) {
        }
    });

I've chosen to use fake-dragging because even the docs say it could be useful for this exact same scenario :

A fake drag can be useful if you want to synchronize the motion of the ViewPager with the touch scrolling of another view, while still letting the ViewPager control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) Call fakeDragBy(float) to simulate the actual drag motion. Call endFakeDrag() to complete the fake drag and fling as necessary.

To make it easy to test, here's more code:

MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final ViewPager viewPager = (ViewPager) findViewById(R.id.viewPager);
    final ViewPager viewPager2 = (ViewPager) findViewById(R.id.viewPager2);
    viewPager.setAdapter(new MyPagerAdapter());
    viewPager2.setAdapter(new MyPagerAdapter());
    //do here what's needed to make "viewPager2" to follow dragging on "viewPager"
    }

private class MyPagerAdapter extends PagerAdapter {
    int[] colors = new int[]{0xffff0000, 0xff00ff00, 0xff0000ff};

    @Override
    public int getCount() {
        return 3;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        ((ViewPager) container).removeView((View) object);
    }

    @Override
    public boolean isViewFromObject(final View view, final Object object) {
        return (view == object);
    }

    @Override
    public Object instantiateItem(final ViewGroup container, final int position) {
        TextView textView = new TextView(MainActivity.this);
        textView.setText("item" + position);
        textView.setBackgroundColor(colors[position]);
        textView.setGravity(Gravity.CENTER);
        final LayoutParams params = new LayoutParams();
        params.height = LayoutParams.MATCH_PARENT;
        params.width = LayoutParams.MATCH_PARENT;
        params.gravity = Gravity.CENTER;
        textView.setLayoutParams(params);
        textView.setTextColor(0xff000000);
        container.addView(textView);
        return textView;
    }
}

activity_main

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="viewPager:"/>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_marginLeft="30dp"
        android:layout_marginRight="30dp"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="viewPager2:"/>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager2"
        android:layout_width="match_parent"
        android:layout_height="100dp"/>

</LinearLayout>

The question

The code almost works well. I just need to know, what is missing to avoid the issues of stopping the dragging (by the user)? The other issues might have easier solutions.

Darwin answered 30/6, 2015 at 12:42 Comment(0)
D
7

A possible solution

After a lot of work, we've found a solution that almost has no bugs. There is a rare bug that while scrolling, the other viewPager flashes. Weird thing is that it happens only when the user scrolls the second viewPager. All other tries had other issues, like empty page or "jumpy"/"rubber" effect when finishing the scrolling to a new page.

Here's the code:

private static class ParallaxOnPageChangeListener implements ViewPager.OnPageChangeListener {
    private final AtomicReference<ViewPager> masterRef;
    /**
     * the viewpager that is being scrolled
     */
    private ViewPager viewPager;
    /**
     * the viewpager that should be synced
     */
    private ViewPager viewPager2;
    private float lastRemainder;
    private int mLastPos = -1;

    public ParallaxOnPageChangeListener(ViewPager viewPager, ViewPager viewPager2, final AtomicReference<ViewPager> masterRef) {
        this.viewPager = viewPager;
        this.viewPager2 = viewPager2;
        this.masterRef = masterRef;
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        final ViewPager currentMaster = masterRef.get();
        if (currentMaster == viewPager2)
            return;
        switch (state) {
            case ViewPager.SCROLL_STATE_DRAGGING:
                if (currentMaster == null)
                    masterRef.set(viewPager);
                break;
            case ViewPager.SCROLL_STATE_SETTLING:
                if (mLastPos != viewPager2.getCurrentItem())
                    viewPager2.setCurrentItem(viewPager.getCurrentItem(), false);
                break;
            case ViewPager.SCROLL_STATE_IDLE:
                masterRef.set(null);
                viewPager2.setCurrentItem(viewPager.getCurrentItem(), false);
                mLastPos = -1;
                break;
        }
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (masterRef.get() == viewPager2)
            return;
        if (mLastPos == -1)
            mLastPos = position;
        float diffFactor = (float) viewPager2.getWidth() / this.viewPager.getWidth();
        float scrollTo = this.viewPager.getScrollX() * diffFactor + lastRemainder;
        int scrollToInt = scrollTo < 0 ? (int) Math.ceil(scrollTo) : (int) Math.floor(scrollTo);
        lastRemainder = scrollToInt - scrollTo;
        if (mLastPos != viewPager.getCurrentItem())
            viewPager2.setCurrentItem(viewPager.getCurrentItem(), false);
        viewPager2.scrollTo(scrollToInt, 0);

    }

    @Override
    public void onPageSelected(int position) {
    }
}

usage:

    /**the current master viewPager*/
    AtomicReference<ViewPager> masterRef = new AtomicReference<>();
    viewPager.addOnPageChangeListener(new ParallaxOnPageChangeListener(viewPager, viewPager2, masterRef));
    viewPager2.addOnPageChangeListener(new ParallaxOnPageChangeListener(viewPager2, viewPager, masterRef));

For the auto-switching, the original code works fine.

Github Project

Since it's quite hard to test it, and I want to make it easier for you guys to try it out, here's a Github repo:

[https://github.com/AndroidDeveloperLB/ParallaxViewPagers][4]

Do note that as I've mentioned, it still has issues. Mainly the "flashing" effect from time to time, but for some reason, only when scrolling on the second ViewPager.

Darwin answered 5/7, 2015 at 22:19 Comment(0)
E
1

I don't think 2 ViewPagers are apt for this. Both encapsulate their own gesture and animation logic, and were not built to be synchronized externally.

You should have a look at the CoordinatorLayout. It is designed specifically to co-ordinate transitions and animations of its child views. Each child view can have a Behaviour and can monitor changes to its dependent sibling views and update its own state.

Extortionary answered 2/7, 2015 at 14:15 Comment(1)
But this is the requirement that I have. There is a viewpager of images at the top, and one at the bottom with texts. The top one is smaller in width compared to the bottom one.Darwin
A
1

Report the first ViewPager's scroll changes to the second ViewPager:

viewPager.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
    @Override
    public void onScrollChanged() {
        viewPager2.scrollTo(viewPager.getScrollX(), viewPager2.getScrollY());
    }
});

With complex pages, this method might make the second pager lag behind.


Edit:

Instead of waiting for the scroll to change, you can report to the method calls directly with a custom class:

public class SyncedViewPager extends ViewPager {
    ...
    private ViewPager mPager;

    public void setSecondViewPager(ViewPager viewPager) {
        mPager = viewPager;
    }

    @Override
    public void scrollBy(int x, int y) {
        super.scrollBy(x, y);
        if (mPager != null) {
            mPager.scrollBy(x, mPager.getScrollY());
        }
    }

    @Override
    public void scrollTo(int x, int y) {
        super.scrollTo(x, y);
        if (mPager != null) {
            mPager.scrollTo(x, mPager.getScrollY());
        }
    }
}

And set the pager.

viewPager.setSecondViewPager(viewPager2);

Note: neither of these methods will invoke the page change listeners on the second pager.
You can just add a listener to the first one and account for both pagers there.

Adamski answered 3/7, 2015 at 8:8 Comment(8)
Have you really checked it? It looks like a very similar thing I've already tried. Please try to play with it a lot. Does the "currentPage" really change for both ViewPagers? Also, if one points to the other, doesn't it mean there will be a never-ending cycle?Darwin
@androiddeveloper a never ending cycle means a circular connection. This is a one way connection. As I've mentioned, listeners are not called that includes the "currentPage". However, that can be avoided by adding listener on the first pager, which I too mentioned.Adamski
Please show a full solution. I've already had partial solutions. They don't work as they should. For easier working on this, here's a Github project I've made just for this: github.com/AndroidDeveloperLB/ParallaxViewPagersDarwin
Also, BTW, your solution doesn't check the ratio that the scrolling should work with. As I've written, the viewpagers have different width. What you did won't work as one ViewPager would finish switching to another page, while the other might not even switch to it. This isn't a Parallax effect. In the Github repo, you will see that when you scroll to the middle, the other ViewPager will scroll to the middle too.Darwin
@androiddeveloper It seems that you already know that this should check the ratio and modify the x coordinate accordingly. Not sure why you want me to write it in the answer. If this didn't have a bounty, I'd vote for a close as it is: "too broad" :)Adamski
What's too broad about it? It's a very specific question.Darwin
@androiddeveloper I didn't say it's unspecific. It's all these requirements that make it too broad to answer shortly.Adamski
Ok. I'm sorry you are upset about it, but it's important as one requirement can harm possible solutions. Can you please check the solution you've written? Maybe use the repo as a comparison ?Darwin
S
0

You can use Paralloid. I have an application with parallex effect. Not a pager but a horizontalschrollbar which is quite the same in your context. This library runs well. Alternatively use parallaxviewpager which I didn't use but looks more towards your direction.

Shushubert answered 3/7, 2015 at 7:47 Comment(2)
I need it to move like viewPagers, and I need to use 2 viewpagers that affect one another.Darwin
Anyway, I've created a Github repo to show you what I'm talking about.Darwin

© 2022 - 2024 — McMap. All rights reserved.