Синхронизация прокрутки нескольких RecyclerViews

У меня есть ViewPager, показывающий фрагмент на странице. Этот фрагмент содержит список элементов внутри RecyclerView. Список элементов всегда имеет тот же размер, и представления для предметов также имеют одинаковую высоту. При прокрутке одного из RecyclerViews я хочу, чтобы другие RecyclerViews прокручивались одновременно и на одинаковом расстоянии. Как синхронизировать прокрутку RecyclerViews?

Ответ 1

Вот мое решение. Чем меньше код, тем лучше...

lvDetail и lvDetail2 - это RecyclerView, которые вы хотите синхронизировать.

    final RecyclerView.OnScrollListener[] scrollListeners = new RecyclerView.OnScrollListener[2];
    scrollListeners[0] = new RecyclerView.OnScrollListener( )
    {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy)
        {
            super.onScrolled(recyclerView, dx, dy);
            lvDetail2.removeOnScrollListener(scrollListeners[1]);
            lvDetail2.scrollBy(dx, dy);
            lvDetail2.addOnScrollListener(scrollListeners[1]);
        }
    };
    scrollListeners[1] = new RecyclerView.OnScrollListener( )
    {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy)
        {
            super.onScrolled(recyclerView, dx, dy);
            lvDetail.removeOnScrollListener(scrollListeners[0]);
            lvDetail.scrollBy(dx, dy);
            lvDetail.addOnScrollListener(scrollListeners[0]);
        }
    };
    lvDetail.addOnScrollListener(scrollListeners[0]);
    lvDetail2.addOnScrollListener(scrollListeners[1]);

Ответ 2

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

Первый вариант, который приходит на ум, - это прослушивание изменений прокрутки на обоих видах прокрутки и, когда один из них прокручивается, используйте scrollBy (int x, int y) на другой. К сожалению, программная прокрутка также вызовет прослушиватель, поэтому вы попадете в цикл.

Чтобы решить эту проблему, вам нужно настроить OnItemTouchListener, который добавляет правильный ScrollListener при касании RecyclerView и удаляет его, когда прокрутка останавливается. Это работает почти безупречно, но если вы быстро переходите в длинный RecyclerView, а затем прокручиваете его еще до того, как он закончится, будет перенесен только первый свиток.

Чтобы обойти это, вам нужно будет убедиться, что OnScrollListener добавляется только в том случае, если RecyclerView не работает.

Посмотрим на источник:

    public class SelfRemovingOnScrollListener extends RecyclerView.OnScrollListener {

    @Override
    public final void onScrollStateChanged(@NonNull final RecyclerView recyclerView, final int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            recyclerView.removeOnScrollListener(this);
        }
    }
}

Это класс, из которого вам нужно расширить свой OnScrollListeners. Это гарантирует, что они будут удалены при необходимости.

Затем у меня есть два слушателя, по одному для каждого RecyclerView:

private final RecyclerView.OnScrollListener mLeftOSL = new SelfRemovingOnScrollListener() {
    @Override
    public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
        super.onScrolled(recyclerView, dx, dy);
        mRightRecyclerView.scrollBy(dx, dy);
    }
}, mRightOSL = new SelfRemovingOnScrollListener() {

    @Override
    public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
        super.onScrolled(recyclerView, dx, dy);
        mLeftRecyclerView.scrollBy(dx, dy);
    }
};

И затем при инициализации вы можете настроить OnItemTouchListeners. Было бы лучше настроить один прослушиватель для всего представления, но RecyclerView не поддерживает это. OnItemTouchListeners все равно не создают проблемы:

    mLeftRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {

        private int mLastY;

        @Override
        public boolean onInterceptTouchEvent(@NonNull final RecyclerView rv, @NonNull final
        MotionEvent e) {
            Log.d("debug", "LEFT: onInterceptTouchEvent");

            final Boolean ret = rv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
            if (!ret) {
                onTouchEvent(rv, e);
            }
            return Boolean.FALSE;
        }

        @Override
        public void onTouchEvent(@NonNull final RecyclerView rv, @NonNull final MotionEvent e) {
            Log.d("debug", "LEFT: onTouchEvent");

            final int action;
            if ((action = e.getAction()) == MotionEvent.ACTION_DOWN && mRightRecyclerView
                    .getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
                mLastY = rv.getScrollY();
                rv.addOnScrollListener(mLeftOSL);
            }
            else {
                if (action == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) {
                    rv.removeOnScrollListener(mLeftOSL);
                }
            }
        }

        @Override
        public void onRequestDisallowInterceptTouchEvent(final boolean disallowIntercept) {
            Log.d("debug", "LEFT: onRequestDisallowInterceptTouchEvent");
        }
    });

    mRightRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {

        private int mLastY;

        @Override
        public boolean onInterceptTouchEvent(@NonNull final RecyclerView rv, @NonNull final
        MotionEvent e) {
            Log.d("debug", "RIGHT: onInterceptTouchEvent");

            final Boolean ret = rv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
            if (!ret) {
                onTouchEvent(rv, e);
            }
            return Boolean.FALSE;
        }

        @Override
        public void onTouchEvent(@NonNull final RecyclerView rv, @NonNull final MotionEvent e) {
            Log.d("debug", "RIGHT: onTouchEvent");

            final int action;
            if ((action = e.getAction()) == MotionEvent.ACTION_DOWN && mLeftRecyclerView
                    .getScrollState
                            () == RecyclerView.SCROLL_STATE_IDLE) {
                mLastY = rv.getScrollY();
                rv.addOnScrollListener(mRightOSL);
            }
            else {
                if (action == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) {
                    rv.removeOnScrollListener(mRightOSL);
                }
            }
        }

        @Override
        public void onRequestDisallowInterceptTouchEvent(final boolean disallowIntercept) {
            Log.d("debug", "RIGHT: onRequestDisallowInterceptTouchEvent");
        }
    });
}

Обратите также внимание на то, что в моем конкретном случае RecyclerView не являются первыми, кто получает событие касания, поэтому мне нужно его перехватить. Если это не ваш случай, вы можете (должен) объединить код из onInterceptTouchEvent (...) в onTouchEvent (...).

Наконец, это приведет к сбою, если ваш пользователь попытается одновременно прокрутить два RecyclerView. Лучшим решением для улучшения качества работы является установка android:splitMotionEvents="false" в прямом родителе, содержащем RecyclerViews.

Вы можете увидеть пример с этим кодом здесь.

Ответ 3

Я думаю, что нашел очень простой и короткий ответ.

как сказал Хорхе Антонио Диас-Бенито: "Первый вариант, который приходит на ум, - это прослушивание изменений прокрутки на обоих видах прокрутки и, когда один из них прокручивается, используйте scrollBy (int x, int y) на другом. К сожалению, программная прокрутка также вызовет прослушиватель, так что вы закончите цикл."

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

Решение

public class SelfScrolListener extends RecyclerView.OnScrollListener {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            viewIsScrolling = -1;
        }
    }
}

Это ваш пользовательский OnScrollListener, чтобы проверить, является ли scrollState IDLE. это правда → никто не прокручивает. поэтому `int viewIsScolling = -1

теперь вам нужно определить, можно ли прокручивать. это код:

int viewIsScrolling = 1;
boolean firstIsTouched = false;
boolean secondIsTouched = false;
SelfScrolListener firstOSL= new SelfScrolListener() {
    @Override
    public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
        super.onScrolled(recyclerView, dx, dy);

        if (firstIsTouched) {
             if (viewIsScrolling == -1) {
                 viewIsScrolling = 0;
             }
             if (viewIsScrolling == 0) {
                 secondRecyclerView.scrollBy(dx, dy);
             }
        }
    }
};
SelfScrolListener secondOSL= new SelfScrolListener() {
    @Override
    public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
        super.onScrolled(recyclerView, dx, dy);
        if(secondIsTouched){
             if (viewIsScrolling == -1) {
                 viewIsScrolling = 1;
             }
             if (viewIsScrolling == 1) {
                 firstRecyclerView.scrollBy(dx, dy);
             }
        }
    }
};
firstRecyclerView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        firstIsTouched= true;
        return false;
    }
});
secondRecyclerView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        secondIsTouched= true;
        return false;
    }
});
firstRecyclerView.addOnScrollListener(firstOSL);
secondRecyclerView.addOnScrollListener(secondOSL);

viewIsScrolling = глобальный int и устанавливается в начале -1; состояние, которое никто не прокручивает. вы можете объявить столько просмотров, сколько захотите.

Ответ 4

Прежде всего рассмотрите возможность использования NestedScrollView в качестве родителя для RecyclerViews. Это может потребовать некоторых дополнительных настроек, но общая идея та же:

<android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</android.support.v4.widget.NestedScrollView>

Если вы не можете сделать это по каким-либо причинам, вы можете синхронизировать прокрутку программно. Просто нужно избегать бесконечного цикла обратного вызова события onScrolled(), как уже упоминалось в других ответах. Другими словами, когда вы начали программную прокрутку, вы ничего не делаете внутри onScroll(), пока не закончите программную прокрутку. И не прокручивайте программно RecyclerView, который был изначально прокручен.

Поместите все корзины для синхронизации в

List<RecyclerView> syncRecyclers

Позвони

addSyncListeners()

Наслаждайтесь

public class SyncScrollActivity extends AppCompatActivity {
    private List<RecyclerView> syncRecyclers;
    private boolean isProgrammaticallyScrolling = false;


    private void addSyncListeners() {
        for (RecyclerView recyclerView : syncRecyclers) {
            recyclerView.addOnScrollListener(new SyncOnScrollListener());
        }
    }

    private class SyncOnScrollListener extends RecyclerView.OnScrollListener {
        @Override
        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
            if (!isProgrammaticallyScrolling) {
                isProgrammaticallyScrolling = true;
                scrollAll(recyclerView, dx, dy);
                isProgrammaticallyScrolling = false;
            }
        }
    }

    private void scrollAll(RecyclerView exceptRecycler, int dx, int dy) {
        for (RecyclerView recyclerView : syncRecyclers) {
            if (!recyclerView.equals(exceptRecycler)) {
                recyclerView.scrollBy(dx, dy);
            }
        }
    }
}

Ответ 5

Создайте переменную в своем пейджер-адаптере, которая будет отвечать за сохранение текущей позиции scrollY для своих страниц, и всякий раз, когда ваш getItem(position) вызовет обновление ListView positoin, посмотрите CacheFragmentStatePagerAdapter в https://github.com/ksoichiro/Android-ObservableScrollView/blob/master/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabActivity.java

Ответ 6

версия kotlin, эта не обрабатывает fling (пока)

class ScrollSynchronizer {
    val boundRecyclerViews: ArrayList<RecyclerView> = ArrayList()
    var verticalOffset = 0

    private val scrollListener = object: RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            // scroll every other rv to the same position
            boundRecyclerViews.filter { it != recyclerView }.forEach { targetView ->
                targetView.removeOnScrollListener(this)
                targetView.scrollBy(dx, dy)
                targetView.addOnScrollListener(this)
            }
        }

        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                verticalOffset = recyclerView.computeVerticalScrollOffset()
            }
        }
    }

    fun add(view: RecyclerView) {
        with (view) {
            if (boundRecyclerViews.contains(view)) {
                removeOnScrollListener(scrollListener)
                boundRecyclerViews.remove(this)
            }
            addOnScrollListener(scrollListener)
            boundRecyclerViews.add(this)
        }
    }

    fun scrollToLastOffset(rv: RecyclerView) {
        rv.removeOnScrollListener(scrollListener)
        val currentOffset = rv.computeVerticalScrollOffset()
        if (currentOffset != verticalOffset) {
            rv.scrollBy(0, verticalOffset - currentOffset)
        }
        rv.addOnScrollListener(scrollListener)
    }

    fun disable() = boundRecyclerViews.forEach { it.removeOnScrollListener(scrollListener) }

    fun enable() = boundRecyclerViews.forEach { it.addOnScrollListener(scrollListener) }

    fun addAll(recyclerViews: List<RecyclerView>) = recyclerViews.forEach { add(it) }
}