Синхронизация двух ViewPager с помощью OnPageChangeListener

Я пытаюсь синхронизировать два ViewPagerс, так как, по-видимому, до меня было довольно много людей, и я дошел до этого:

private ViewPager mNavPager;

private ViewPager mMainPager;

private final OnPageChangeListener mNavPagerListener = new OnPageChangeListener() {

    private boolean mNavDragging;
    private int mScrollPosition;

    @Override
    public void onPageSelected(int position) {
        mScrollPosition = position;
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if(mNavDragging)
            mMainPager.scrollTo(positionOffsetPixels, 0);
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        switch(state) {
        case ViewPager.SCROLL_STATE_DRAGGING:
        case ViewPager.SCROLL_STATE_SETTLING:
            mNavDragging = true;
            break;
        case ViewPager.SCROLL_STATE_IDLE:
            mNavDragging = false;
            break;
        }
    }
};

private OnPageChangeListener mMainPagerListener = new OnPageChangeListener() {

    private boolean mMainDragging;
    private int mScrollPosition;

    @Override
    public void onPageSelected(int position) {
        mScrollPosition = position;
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if(mMainDragging)
            mNavPager.scrollTo(positionOffsetPixels, 0);
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        switch(state) {
        case ViewPager.SCROLL_STATE_DRAGGING:
        case ViewPager.SCROLL_STATE_SETTLING:
            mMainDragging = true;
            break;
        case ViewPager.SCROLL_STATE_IDLE:
            mMainDragging = false;
            break;
        }
    }
};

Если один из них прокручивается вручную, другой подчиняется ему с помощью свойства состояния прокрутки. Он прекрасно работает, пока предметы не достигнут своей окончательной позиции; в этот момент подчиненный пейджер мгновенно возвращается к ранее выбранному элементу, как будто прокрутка не произошла.

Я пробовал звонить ViewPager#setCurrentItem(mScrolledPosition) с множеством различных логических ограничений, но это тоже не работает, хотя иногда делает его еще хуже. Я чувствую, что должно быть что-то, что можно сделать с этим, но я не уверен, что.

Как я могу заставить подчиненный пейджер оставаться в правильном положении?

6 ответов

Я решил эту проблему намного проще (и чище), используя OnPageChangeListener:

mViewPager1.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {

      private int mScrollState = ViewPager.SCROLL_STATE_IDLE;

      @Override
      public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
        if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
          return;
        }
        mViewPager2.scrollTo(mViewPager1.getScrollX(), mViewPager2.getScrollY());
      }

      @Override
      public void onPageSelected(final int position) {

      }

      @Override
      public void onPageScrollStateChanged(final int state) {
        mScrollState = state;
        if (state == ViewPager.SCROLL_STATE_IDLE) {
          mViewPager2.setCurrentItem(mViewPager1.getCurrentItem(), false);
        }
      }
});

mViewPager2.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {

      private int mScrollState = ViewPager.SCROLL_STATE_IDLE;

      @Override
      public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
        if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
          return;
        }
        mViewPager1.scrollTo(mViewPager2.getScrollX(), mViewPager1.getScrollY());
      }

      @Override
      public void onPageSelected(final int position) {

      }

      @Override
      public void onPageScrollStateChanged(final int state) {
        mScrollState = state;
        if (state == ViewPager.SCROLL_STATE_IDLE) {
          mViewPager1.setCurrentItem(mViewPager2.getCurrentItem(), false);
        }
      }
});

Я решил проблему без использования Слушателя. Таким образом, вы можете использовать слушатель для некоторых других вещей и сделать код более чистым.

Я знаю, это вопрос, заданный давно. Я искал решение и решил его сам.

Вот как мой код пользовательского ViewPager

public class CustomPager extends ViewPager {
CustomPager mCustomPager;
private boolean forSuper;

public CustomPager(Context context) {
    super(context);
}

public CustomPager(Context context, AttributeSet attrs) {
    super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent arg0) {
    if (!forSuper) {

        mCustomPager.forSuper(true);
        mCustomPager.onInterceptTouchEvent(arg0);
        mCustomPager.forSuper(false);
    }
    return super.onInterceptTouchEvent(arg0);
}

@Override
public boolean onTouchEvent(MotionEvent arg0) {
    if (!forSuper) {
        mCustomPager.forSuper(true);
        mCustomPager.onTouchEvent(arg0);
        mCustomPager.forSuper(false);
    }
    return super.onTouchEvent(arg0);
}

public void setViewPager(CustomPager customPager) {
    mCustomPager = customPager;
}

public void forSuper(boolean forSuper) {
    this.forSuper = forSuper;
}

@Override
public void setCurrentItem(int item, boolean smoothScroll) {
    if (!forSuper) {
        mCustomPager.forSuper(true);
        mCustomPager.setCurrentItem(item, smoothScroll);
        mCustomPager.forSuper(false);
    }
    super.setCurrentItem(item, smoothScroll);
}

@Override
public void setCurrentItem(int item) {
    if (!forSuper) {
        mCustomPager.forSuper(true);
        mCustomPager.setCurrentItem(item);
        mCustomPager.forSuper(false);
    }
    super.setCurrentItem(item);

}

}

И вы должны установить пейджеры, как это до установки адаптера и сразу после получения ссылки с использованием идентификатора.

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    backPager=(CustomPager) findViewById(R.id.BackPager);
    frontPager = (CustomPager) findViewById(R.id.frontPager);
    frontPager.setViewPager(backPager);
    backPager.setViewPager(frontPager);
    backPager.setAdapter(your Adapter);
    frontPager.setAdapter(your Adapter);
}

Необходимо установить ViewPager перед назначением адаптера.

У меня была похожая проблема, также нужно было объединить мою синхронизацию с анимацией в PageTransformer,

Первоначально опубликовал этот ответ здесь: /questions/46791576/android-2-viewpagers-odnovremennaya-prokrutka/46791601#46791601

Но я буду вставлять это здесь для удобства.

Решение, которое работало лучше всего для меня, состояло в том, чтобы пройти MotionEvent в OnTouchListener между ViewPager экземпляров. Пробовал фальшивое перетаскивание, но оно всегда было медленным и глючным - мне нужен был плавный эффект параллакса.

Итак, мой совет заключается в реализации View.OnTouchListener, MotionEvent должен быть масштабирован, чтобы компенсировать разницу в ширине.

public class SyncScrollOnTouchListener implements View.OnTouchListener {

private final View syncedView;

public SyncScrollOnTouchListener(@NonNull View syncedView) {
    this.syncedView = syncedView;
}

@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
    MotionEvent syncEvent = MotionEvent.obtain(motionEvent);
    float width1 = view.getWidth();
    float width2 = syncedView.getWidth();

    //sync motion of two view pagers by simulating a touch event
    //offset by its X position, and scaled by width ratio
    syncEvent.setLocation(syncedView.getX() + motionEvent.getX() * width2 / width1,
            motionEvent.getY());
    syncedView.onTouchEvent(syncEvent);
    return false;
}
}

Затем установите его на свой ViewPager

    sourcePager.setOnTouchListener(new SyncScrollOnTouchListener(targetPager));

Обратите внимание, что это решение будет работать, только если оба пейджера имеют одинаковую ориентацию. Если вам это нужно для работы в разных ориентациях - настройте syncEvent Координата Y вместо X.

Есть еще одна проблема, которую мы должны принять во внимание - минимальная скорость перемещения и расстояние, при котором только один пейджер может изменить страницу.

Это можно легко исправить, добавив OnPageChangeListener на наш пейджер

sourcePager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset,
                                   int positionOffsetPixels) {
            //no-op
        }

        @Override
        public void onPageSelected(int position) {
            targetPager.setCurrentItem(position, true);
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            //no-op
        }
    }); 

Это делает все правильно, за исключением того, что иногда пропускает очень быстрые щелчки на ведомом виде. По какой-то причине включение фальшивых событий перетаскивания на этапе установления вызывает реальные проблемы.

private ViewPager mNavPager;

private ViewPager mMainPager;

private final OnPageChangeListener mNavPagerListener = new OnPageChangeListener() {

    private int mLastScrollPosition;
    private int mLastPagePosition;

    @Override
    public void onPageSelected(int position) {
        mLastPagePosition = position;
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if(mMainPager.isFakeDragging()) {
            int absoluteOffsetPixels = positionOffsetPixels;
            if(mLastPagePosition!=position) {
                absoluteOffsetPixels += (position - mLastPagePosition) * mMainPager.getWidth();
                mLastPagePosition = position;
            }
            Log.d(TAG, "fake nav drag by " + (mLastScrollPosition - absoluteOffsetPixels));
            mMainPager.fakeDragBy(mLastScrollPosition - absoluteOffsetPixels);
            mLastScrollPosition = positionOffsetPixels;
        }
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        if(!mNavPager.isFakeDragging()) {
            switch(state) {
            case ViewPager.SCROLL_STATE_DRAGGING:
                if(!mMainPager.isFakeDragging())
                    mMainPager.beginFakeDrag();
                break;
            case ViewPager.SCROLL_STATE_SETTLING:
            case ViewPager.SCROLL_STATE_IDLE:
                if(mMainPager.isFakeDragging()) {
                    mMainPager.endFakeDrag();
                    mLastScrollPosition = 0;
                }
                break;
            }
        }
    }
};

private OnPageChangeListener mMainPagerListener = new OnPageChangeListener() {

    private int mLastScrollPosition;
    private int mLastPagePosition;

    @Override
    public void onPageSelected(int position) {
        mLastPagePosition = position;
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if(mNavPager.isFakeDragging()) {
            int absoluteOffsetPixels = positionOffsetPixels;
            if(mLastPagePosition!=position) {
                absoluteOffsetPixels += (position - mLastPagePosition) * mMainPager.getWidth();
                mLastPagePosition = position;
            }
            Log.d(TAG, "fake nav drag by " + (mLastScrollPosition - absoluteOffsetPixels));
            mNavPager.fakeDragBy(mLastScrollPosition - absoluteOffsetPixels);
            mLastScrollPosition = positionOffsetPixels;
        }
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        if(!mMainPager.isFakeDragging()) {
            switch(state) {
            case ViewPager.SCROLL_STATE_DRAGGING:
                if(!mNavPager.isFakeDragging())
                    mNavPager.beginFakeDrag();
                break;
            case ViewPager.SCROLL_STATE_SETTLING:
            case ViewPager.SCROLL_STATE_IDLE:
                if(mNavPager.isFakeDragging()) {
                    mNavPager.endFakeDrag();
                    mLastScrollPosition = 0;
                }
                break;
            }
        }
    }
};

РЕДАКТИРОВАТЬ

Теперь я считаю, что это невозможно без какого-либо довольно существенного пользовательского кода. Причина в том, что оба ViewPagerс VelocityTracker внутри, который управляет прокруткой. Так как MotionEvents, переданные в, не могут быть переданы VelocityTracker в то же относительное время для каждого пейджера, трекеры будут иногда делать разные выводы о том, как реагировать.

Тем не менее, можно использовать модифицированный PagerTitleStripчтобы получить точное отслеживаниеViewPager и передавать сенсорные события, захваченные полосой, непосредственно на ViewPager,

Источник дляPagerTitleStrip это здесь.

В целом, что нужно сделать, чтобы сделать эту работу следующим образом: заменить mPrevText, mCurrText а также mNextText с представлениями типа, который вы хотите использовать; удалить onAttachedToWindow() а также onDetachedFromWindow() функции; удалить звонки на PagerAdapter которые имеют дело с наблюдателями набора данных, и добавить OnTouchListener на модифицированную полосу, которая подделка тащит основной пейджер. Вам также необходимо добавить измененную полосу заголовка в качестве OnPageChangeListener к ViewPager поскольку внутренние слушатели не видны за пределами пакета.

Это гигантская боль? Да. Но это работает. Я напишу это более подробно в ближайшее время.

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

    public class OnPageChangeListernerSync implements ViewPager.OnPageChangeListener {

    private ViewPager master;
    private ViewPager slave;
    private int mScrollState = ViewPager.SCROLL_STATE_IDLE;

    public OnPageChangeListernerSync(ViewPager master, ViewPager slave){
        this.master = master;
        this.slave = slave;
    }

    @Override
    public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
        if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
            return;
        }
        this.slave.scrollTo(this.master.getScrollX()*
                this.slave.getWidth()/
                this.master.getWidth(), 0);
    }

    @Override
    public void onPageSelected(final int position) {

    }

    @Override
    public void onPageScrollStateChanged(final int state) {
        mScrollState = state;
        if (state == ViewPager.SCROLL_STATE_IDLE) {
            this.slave.setCurrentItem(this.master
                    .getCurrentItem(), false);
        }
    }
}

...

// In your activity
this.upperPager.addOnPageChangeListener(new OnPageChangeListernerSync(this.upperPager, this.lowerPager));

this.lowerPager.addOnPageChangeListener(new OnPageChangeListernerSync(this.lowerPager, this.upperPager));

Все Ответы примерно точны в некоторой ситуации. Здесь я даю еще один ответ, который будет использоваться для одновременного перемещения обоих ViewPager, независимо от того, там ли размер или нет:

viewPagerBanner.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    private int scrollState = ViewPager.SCROLL_STATE_IDLE;
    // Indicates that the pager is in an idle, settled state. 
    // The current page is fully in view and no animation is in progress.

    @Override
    public void onPageScrolled(int position, float positionOffset, 
                               int positionOffsetPixels) {
        if (scrollState == ViewPager.SCROLL_STATE_IDLE) {
            return;
        }
        viewPagerTitle.scrollTo(viewPagerBanner.getScrollX()*
                                viewPagerTitle.getWidth()/
                                viewPagerBanner.getWidth(), 0);
        // We are not interested in Y axis position
    }

    @Override
    public void onPageSelected(int position) {}

    @Override
    public void onPageScrollStateChanged(int state) {
        scrollState = state;
        if (state == ViewPager.SCROLL_STATE_IDLE) {
            viewPagerTitle.setCurrentItem(viewPagerBanner.getCurrentItem(), false);
        }
    }
});
Другие вопросы по тегам