如何在 2 个 ViewPagers 之间产生视差效果?

How to have a parallax effect between 2 ViewPagers?

背景

考虑以下场景:

  1. 有 2 个 viewPager,每个的宽度和高度都不同,但它们的页面数完全相同。
  2. 拖一个应该拖另一个,所以效果就像 "parallax" 效果。
  3. 与#2 相同,但用于另一个 viewPager。
  4. viewpager 之间也有一个自动切换机制,所以每 2 秒它会自动切换到下一页(如果在最后一页,切换回第一页)。

问题

我想我的一些功能可以正常工作,但它有一些问题:

  1. 这很简单XML。这里没什么特别的。
  2. 我使用了 'OnPageChangeListener' 并且在 'onPageScrolled' 中,我使用了假拖动。问题是,如果用户的拖动在页面中间附近停止(从而激活自动滚动 viewPager left/right),假拖动会失去同步,并且可能会发生奇怪的事情:错误的页面甚至停留在中间页数...
  3. 目前,我不处理用户拖动的另一个 viewPager,但这也可能是一个问题。
  4. 当我成功解决#2(或#3)时,这可能是个问题。现在,我正在使用一个使用可运行的处理程序并在内部调用此命令:

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

我试过的

我试过 this post,但效果不佳,因为当用户拖动在完成切换到另一个页面之前停止时,它遇到了同样的问题。

唯一有效的类似解决方案是使用单个 ViewPager (here),但我需要使用 2 个 ViewPager,每个都会影响另一个。

所以我尝试自己做:假设一个 viewPager 是 "viewPager",另一个(应该跟在 "viewPager" 之后)是 "viewPager2",这就是我已完成:

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

我选择使用假拖动,因为 even the docs 说它对完全相同的场景很有用:

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.

为了便于测试,这里有更多代码:

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>

问题

代码几乎可以正常工作。我只需要知道,为了避免(用户)停止拖动的问题,缺少什么? 其他问题可能有更简单的解决方案。

我认为 2 个 ViewPagers 不适合这个。两者都封装了自己的手势和动画逻辑,并不是为了与外部同步而构建的。

您应该看看 CoordinatorLayout. It is designed specifically to co-ordinate transitions and animations of its child views. Each child view can have a Behaviour 并且可以监视对其依赖的兄弟视图的更改并更新其自己的状态。

你可以用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,我没用过,但看起来更像你的方向。

将第一个 ViewPager 的滚动更改报告给第二个 ViewPager:

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

对于复杂的页面,此方法可能会使第二个寻呼机滞后。


编辑:

不用等待滚动改变,你可以直接用自定义的方法调用报告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());
        }
    }
}

并设置寻呼机。

viewPager.setSecondViewPager(viewPager2);

注意:这些方法都不会调用第二个寻呼机上的页面更改侦听器。
您可以只向第一个寻呼机添加一个侦听器并考虑两个寻呼机都在那里。

可能的解决方案

经过大量工作,我们找到了一个几乎没有错误的解决方案。有一个罕见的错误,在滚动时,另一个 viewPager 会闪烁。奇怪的是,它仅在用户滚动第二个 viewPager 时发生。 所有其他尝试都有其他问题,例如空白页面或完成滚动到新页面时的 "jumpy"/"rubber" 效果。

代码如下:

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

用法:

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

对于自动切换,原始代码可以正常工作。

Github 项目

由于很难测试,我想让你们更容易尝试,这里是一个Github repo:

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

请注意,正如我所提到的,它仍然存在问题。主要是时不时的"flashing"效果,但是不知道为什么,只有在第二个ViewPager上滚动的时候才出现。