【问题标题】:How to have a parallax effect between 2 ViewPagers?如何在 2 个 ViewPager 之间产生视差效果?
【发布时间】:2021-05-28 05:42:50
【问题描述】:

背景

考虑以下场景:

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

问题

我认为我的某些功能可以正常工作,但存在一些问题:

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

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

我尝试过的

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

只有一个有效的类似解决方案是使用单个 ViewPager (here),但我需要使用 2 个 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) {
        }
    });

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

如果您想同步 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>

问题

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

【问题讨论】:

    标签: android android-viewpager parallax


    【解决方案1】:

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

    你应该看看CoordinatorLayout。它专门设计用于协调其子视图的过渡和动画。每个子视图都可以有一个Behaviour,并且可以监视对其依赖的兄弟视图的更改并更新自己的状态。

    【讨论】:

    • 但这是我的要求。顶部有一个图像浏览器,底部有一个带有文本的浏览器。与底部相比,顶部的宽度更小。
    【解决方案2】:

    您可以使用Paralloid。我有一个具有视差效果的应用程序。不是寻呼机,而是水平滚动条,在您的上下文中完全一样。这个库运行良好。或者使用parallaxviewpager,我没有使用,但看起来更接近你的方向。

    【讨论】:

    • 我需要它像 viewPagers 一样移动,而且我需要使用 2 个相互影响的 viewpagers。
    • 无论如何,我已经创建了一个 Github 存储库来向您展示我在说什么。
    【解决方案3】:

    报告第一个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);
    

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

    【讨论】:

    • 你真的检查过吗?它看起来像我已经尝试过的非常相似的事情。请尽量多玩。两个 ViewPager 的“currentPage”是否真的发生了变化?另外,如果一个指向另一个,是不是意味着一个永无止境的循环?
    • @androiddeveloper 一个永无止境的循环意味着一个循环连接。这是一种单向连接。正如我所提到的,不调用包含“currentPage”的侦听器。但是,可以通过在第一个寻呼机上添加侦听器来避免这种情况,我也提到过。
    • 请展示完整的解决方案。我已经有了部分解决方案。他们没有按应有的方式工作。为了更轻松地解决这个问题,这是我为此专门制作的一个 Github 项目:github.com/AndroidDeveloperLB/ParallaxViewPagers
    • 另外,顺便说一句,您的解决方案不会检查滚动应使用的比率。正如我所写的,viewpagers 有不同的宽度。您所做的将不起作用,因为一个 ViewPager 会完成切换到另一个页面,而另一个可能甚至不会切换到它。这不是视差效果。在 Github repo 中,你会看到当你滚动到中间时,另一个 ViewPager 也会滚动到中间。
    • @androiddeveloper 看来你已经知道这应该检查比率并相应地修改x坐标。不知道您为什么要 me 将其写在答案中。如果这没有赏金,我会投票赞成关闭,因为它是:“太宽泛”:)
    【解决方案4】:

    一个可能的解决方案

    经过大量工作,我们找到了一个几乎没有错误的解决方案。有一个罕见的错误,即在滚动时,另一个 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 repo:

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

    请注意,正如我所提到的,它仍然存在问题。主要是不时出现的“闪烁”效果,但由于某种原因,只有在第二个ViewPager上滚动时才出现。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多