Как получить эффект параллакса между 2 ViewPagers?

Фон

Рассмотрим следующий сценарий:

  • Есть 2 viewPagers, каждый из которых имеет разную ширину и высоту, но оба имеют одинаковое количество страниц.
  • перетаскивание нужно перетащить другим, поэтому эффект подобен эффекту "параллакса".
  • то же, что и # 2, но для другого viewPager.
  • также существует механизм автоматического переключения между диспетчерами, поэтому каждые 2 секунды он автоматически переключается на следующую страницу (и если на последней странице переключается на первый).

Проблема

Я думаю, что у меня есть некоторые функциональные возможности, но у него есть некоторые проблемы:

  • Это просто простой XML. ничего особенного здесь.
  • Я использовал 'OnPageChangeListener', и там, в 'onPageScrolled', я использовал поддельное перетаскивание. Проблема в том, что если перетаскивание пользователя останавливается около середины страницы (таким образом активируется автоматическая прокрутка viewPager влево/вправо), поддельное перетаскивание теряет синхронизацию, и могут возникать странные вещи: неправильная страница или даже пребывание между страницами...
  • В настоящее время я не обрабатываю другой viewPager с перетаскиванием пользователя, но это тоже может быть проблемой.
  • Это может быть проблемой, когда мне удается решить проблему №2 (или # 3). На данный момент я использую обработчик, который использует runnable и вызывает эту команду внутри:

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

Что я пробовал

Я пробовал этот пост, но он не сработал, так как он получил те же проблемы, когда пользователь перетаскивал останавливается, прежде чем закончить переключение на другую страницу.

Только одно подобное решение работает с одним ViewPager ( здесь), но мне нужно работать с 2 ViewPagers, каждый влияет на другой.

Итак, я попытался сделать это сам: предположим, что один 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 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>

Вопрос

Код работает очень хорошо. Мне просто нужно знать, чего не хватает, чтобы избежать проблем с перетаскиванием (пользователем)? Другие проблемы могут иметь более простые решения.

Ответы

Ответ 1

Возможное решение

После большой работы мы нашли решение, в котором почти нет ошибок. Существует редкая ошибка, которая во время прокрутки мигает другой 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.

Ответ 2

Я не думаю, что 2 ViewPagers подходят для этого. Оба инкапсулируют свою собственную логику жестов и анимации и не были созданы для синхронизации снаружи.

Вы должны посмотреть на CoordinatorLayout. Он разработан специально для координации переходов и анимаций его детских представлений. Каждое дочернее представление может иметь Behaviour и может отслеживать изменения в зависимых друг от друга представлениях и обновлять собственное состояние.

Ответ 3

Отправлять первые ViewPager изменения прокрутки ко второй ViewPager:

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

С помощью сложных страниц этот метод может сделать второй пейджер отстающим.


Edit:

Вместо ожидания изменения прокрутки вы можете напрямую сообщать о вызовах метода с помощью специального класса:

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

Примечание. ни один из этих методов не будет вызывать слушателей смены страницы на втором пейджере.
Вы можете просто добавить слушателя к первому и учесть для обоих пейджеров.

Ответ 4

Вы можете использовать Paralloid. У меня есть приложение с эффектом parallex. Не пейджер, а горизонтальная панель, которая в вашем контексте совершенно одинакова. Эта библиотека работает хорошо. В качестве альтернативы используйте parallaxviewpager, который я не использовал, но больше смотрю в сторону вашего направления.