如何在两个ViewPager之间实现视差效果?

16

背景

考虑以下情况:

  1. 有两个viewPager,它们的宽度和高度不同,但它们都拥有完全相同数量的页面。
  2. 在一个viewPager上滑动应该会使另一个viewPager随之滑动,这样就像“视差”效果。
  3. 与#2相同,但是针对其他viewPager。
  4. 还有一个自动切换机制,在viewPagers之间每2秒自动切换到下一页(如果在最后一页,则切换回第一页)。

问题

我认为我已经实现了一些功能,但它有一些问题:

  1. 这只是简单的XML,没有什么特别的。
  2. 我使用了“OnPageChangeListener”,并在其中的“onPageScrolled”中使用了伪拖动。问题在于,如果用户的拖动停在页面的中心附近(从而激活自动滚动viewPager左/右),则伪拖动会失去同步,可能会出现奇怪的事情:错误的页面甚至停留在页面之间...
  3. 目前,我不处理用户拖动的另一个viewPager,但这也可能是一个问题。
  4. 当我解决#2(或#3)时,这可能是一个问题。目前,我使用一个处理程序,它使用可运行的方法,并调用此命令:

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

我尝试过的方法

我尝试了这篇文章,但它并没有很好地工作,因为当用户拖动停止时,它会出现相同的问题,导致页面未完全切换到另一页。

唯一有效的类似解决方案是使用单个ViewPager (在这里),但我需要使用两个ViewPager,每个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) {
        }
    });
我选择使用虚拟拖动,因为即使文档也表示在这种情况下它可能非常有用:

如果您想将 ViewPager 的动作与另一个视图的触摸滚动同步,同时仍然让 ViewPager 控制捕捉动作和快速滑动的行为,则虚拟拖动可能非常有用。 (例如视差滚动标签。)调用 fakeDragBy(float) 模拟实际拖动动作。 调用 endFakeDrag() 完成虚拟拖动并根据需要进行快速滑动。

为了方便测试,这里是更多的代码:

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>

问题

这段代码基本上可以正常工作。 我只需要知道,为了避免用户停止拖动,还缺少什么内容? 其他问题可能有更简单的解决方案。

4个回答

7

一种可能的解决方案

经过大量努力,我们找到了一个几乎没有缺陷的解决方案。有一个罕见的 bug,在滚动时另一个 viewPager 会闪烁。奇怪的是,这只会在用户滚动第二个 viewPager 时发生。 所有其他尝试都存在其他问题,例如空白页面或当滚动到新页面时出现“跳跃”/“橡皮”效果。

以下是代码:

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仓库:

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

需要注意的是,正如我所提到的,仍然存在问题。主要是第二个ViewPager在滚动时会间歇性出现“闪烁”效果,但原因不明。


1
我认为两个ViewPagers不适合这种情况。它们都封装了自己的手势和动画逻辑,并不是为了在外部进行同步而构建的。
你应该看看CoordinatorLayout。它专门设计用于协调其子视图的过渡和动画。每个子视图都可以有一个Behavior,并且可以监视其依赖的兄弟视图的更改并更新自身状态。

但这是我所要求的。顶部有一个图片的ViewPager,底部有一个带文本的ViewPager。顶部的ViewPager宽度比底部的ViewPager小。 - android developer

1

将第一个 ViewPager 的滚动变化报告给第二个 ViewPager:

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

在复杂页面中,这种方法可能会使第二页的响应速度变慢。


编辑:

不必等待滚动条改变,您可以直接使用自定义类报告方法调用:

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

注意:这两种方法都不会调用第二个页面更改监听器。
您只需向第一个添加侦听器,然后在那里考虑两个页面。

你真的检查过了吗?它看起来非常类似我已经尝试过的东西。请尽量多地尝试一下。 "currentPage" 对于两个 ViewPagers 真的会改变吗?而且,如果一个指向另一个,这不意味着会有一个无休止的循环吗? - android developer
@androiddeveloper 永无止境的循环意味着循环连接。这是单向连接。正如我所提到的,监听器不包括“currentPage”。但是,可以通过在第一个分页器上添加监听器来避免这种情况,这也是我提到的。 - Simas
请展示一个完整的解决方案。我已经有了部分解决方案,但它们不能像应该的那样工作。为了更轻松地处理这个问题,这里有一个我专门为此创建的 Github 项目:https://github.com/AndroidDeveloperLB/ParallaxViewPagers - android developer
另外,顺便提一下,你的解决方案没有检查滚动应该使用的比例。正如我所写的,视图页面具有不同的宽度。你所做的不会起作用,因为一个ViewPager可能已经完成了切换到另一页,而另一个ViewPager甚至可能还没有切换到它。这不是视差效果。在Github存储库中,当你滚动到中间时,另一个ViewPager也会滚动到中间。 - android developer
@androiddeveloper 看起来你已经知道这个应该检查比率并相应地修改x坐标。不确定为什么你想让在答案中写它。如果这没有赏金,我会投票关闭它,因为它太宽泛了 :) - Simas
显示剩余3条评论

0

你可以使用Paralloid。我有一个带有视差效果的应用程序。不是一个分页器,而是一个水平滚动条,在你的上下文中相当相似。这个库运行良好。或者使用parallaxviewpager,我没有使用过,但看起来更符合你的方向。


我需要它像viewPagers一样移动,并且我需要使用两个相互影响的viewpagers。 - android developer
无论如何,我已经创建了一个Github仓库来展示我所说的内容。 - android developer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接