Синхронизация двух ViewPagers с помощью OnPageChangeListener
Я пытаюсь синхронизировать два ViewPager
s, поскольку, по-видимому, у меня довольно много людей, и я дошел до этого:
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)
множеством различных логических ограничений, но это тоже не работает, хотя иногда это делает хуже. Мне кажется, что с этим нужно что-то сделать, но я не уверен, что.
Как я могу заставить подчиненный пейджер оставаться в правильном положении?
Ответы
Ответ 1
Я решил эту проблему гораздо проще (и чище) с помощью 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);
}
}
});
Ответ 2
Я решил проблему без использования Listener.
Таким образом, вы можете использовать прослушиватель для некоторых других вещей и сделать код более чистым.
Я знаю его вопрос, заданный давно. Я искал решение и сам решил.
Вот как мой пользовательский код 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);
}
Перед назначением адаптера необходимо установить ViewPagers.
Ответ 3
Это делает все правильно, за исключением того, что иногда он пропускает очень быстрые щелчки на слабом представлении. По какой-то причине, включая фальшивые события перетаскивания во время фазы расчета, возникают реальные проблемы.
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
внутри, который контролирует прокрутку. Поскольку передаваемый MotionEvent
не может быть передан в VelocityTracker
в те же относительные моменты времени для каждого пейджера, трекеры иногда будут делать разные выводы о том, как реагировать.
Однако для получения точного отслеживания ViewPager
можно использовать модифицированный PagerTitleStrip
, а также передать события касания, захваченные полосой, непосредственно в ViewPager
.
Источник PagerTitleStrip
находится здесь.
В целом, что нужно сделать, чтобы выполнить эту работу, выполните следующие действия: замените mPrevText
, mCurrText
и mNextText
на представления типа, который вы хотите использовать; удалите функции onAttachedToWindow()
и onDetachedFromWindow()
; удалите вызовы PagerAdapter
, которые имеют дело с наблюдателями набора данных, и добавьте OnTouchListener
к модифицированной полосе, которая поддельно перетаскивает основной пейджер. Вам также необходимо добавить измененную полосу заголовка как OnPageChangeListener
в ViewPager
, так как внутренние прослушиватели не видны вне пакета.
Это гигантская боль? Да. Но это работает. Я напишу об этом более подробно в ближайшее время.
Ответ 4
С помощью всех других ответов я создал класс, чтобы его можно было легко реализовать.
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));
Ответ 5
У меня была аналогичная проблема, также мне нужно было объединить синхронизацию с анимацией в PageTransformer
.
Первоначально опубликованный ответ здесь: fooobar.com/questions/384054/...
Но я приложу его сюда для удобства.
Лучшим решением для меня было передать 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
}
});
Ответ 6
В любой ситуации все ответы примерно точны. Здесь я даю еще один ответ, который будет использоваться для одновременно перемещать оба ViewPagers, независимо от того, является ли этот размер таким же или нет.:
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);
}
}
});