Синхронизация двух 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
внутри, который управляет прокруткой. Так как MotionEvent
s, переданные в, не могут быть переданы 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);
}
}
});