Фон
У меня есть 2 экземпляра RecyclerView. Один из них горизонтальный, второй - вертикальный.
Оба они показывают одни и те же данные и имеют одинаковое количество элементов, но по-разному, а ячейки не обязательно равны по размеру через каждый из них.
Я хочу, чтобы прокрутка в одном будет синхронизироваться с другим, так что первый элемент, показанный на одном, всегда будет отображаться на другом (как первый).
Проблема
Несмотря на то, что мне удалось синхронизировать (я просто выбираю, какой из них является "мастером", чтобы управлять прокруткой другого), направление прокрутки, похоже, влияет на способ его работы.
Предположим, что на данный момент ячейки имеют равный вес:
Если я прокручиваю вверх/влево, он работает так, как я предполагал, более или менее:
Однако, если я прокручиваю вниз/вправо, это позволяет другому RecyclerView показывать первый элемент другого, но обычно не как первый элемент:
Примечание: на приведенных выше снимках экрана я прокрутил в нижней части RecyclerView, но аналогичный результат будет с верхним.
Ситуация становится намного хуже, если, как я уже писал, ячейки имеют разные размеры:
Что я пробовал
Я пытался использовать другие способы прокрутки и перехода в другие позиции, но все попытки не работают.
Использование smoothScrollToPosition сделало вещи еще хуже (хотя это кажется более приятным), потому что, если я брошу, в какой-то момент другой RecyclerView будет контролировать тот, с которым я взаимодействовал.
Я думаю, что я должен использовать направление прокрутки вместе с отображаемыми в данный момент элементами на другом RecyclerView.
Здесь текущий (примерный) код. Обратите внимание, что в реальном коде ячейки могут иметь одинаковые размеры (некоторые из них высокие, некоторые короткие, и т.д.). Одна из строк в коде делает ячейки разной высоты.
activity_main.xml
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/topReccyclerView" android:layout_width="0dp" android:layout_height="100dp"
android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp"
android:orientation="horizontal" app:layoutManager="android.support.v7.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/horizontal_cell"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/bottomRecyclerView" android:layout_width="0dp" android:layout_height="0dp"
android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="8dp" app:layoutManager="android.support.v7.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/topReccyclerView"
tools:listitem="@layout/horizontal_cell"/>
</android.support.constraint.ConstraintLayout>
horizontal_cell.xml
<TextView
android:id="@+id/textView" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="100dp" android:layout_height="100dp"
android:gravity="center" tools:text="@tools:sample/lorem"/>
vertical_cell.xml
<TextView
android:id="@+id/textView" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="50dp"
android:gravity="center" tools:text="@tools:sample/lorem"/>
MainActivity
class MainActivity : AppCompatActivity() {
var masterView: View? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inflater = LayoutInflater.from(this)
topReccyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder.itemView as TextView).text = position.toString()
holder.itemView.setBackgroundColor(if(position%2==0) 0xffff0000.toInt() else 0xff00ff00.toInt())
}
override fun getItemCount(): Int {
return 100
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.horizontal_cell, parent, false)) {}
}
}
bottomRecyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val baseHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50f, resources.displayMetrics).toInt()
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder.itemView as TextView).text = position.toString()
holder.itemView.setBackgroundColor(if(position%2==0) 0xffff0000.toInt() else 0xff00ff00.toInt())
// this makes the heights of the cells different from one another:
holder.itemView.layoutParams.height = baseHeight + (if (position % 3 == 0) 0 else baseHeight / (position % 3))
}
override fun getItemCount(): Int {
return 100
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.vertical_cell, parent, false)) {}
}
}
LinearSnapHelper().attachToRecyclerView(topReccyclerView)
LinearSnapHelper().attachToRecyclerView(bottomRecyclerView)
topReccyclerView.addOnScrollListener(OnScrollListener(topReccyclerView, bottomRecyclerView))
bottomRecyclerView.addOnScrollListener(OnScrollListener(bottomRecyclerView, topReccyclerView))
}
inner class OnScrollListener(private val thisRecyclerView: RecyclerView, private val otherRecyclerView: RecyclerView) : RecyclerView.OnScrollListener() {
var lastItemPos: Int = Int.MIN_VALUE
val thisRecyclerViewId = resources.getResourceEntryName(thisRecyclerView.id)
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
Log.d("AppLog", "onScrollStateChanged:$thisRecyclerViewId $newState")
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> if (masterView == null) {
Log.d("AppLog", "setting $thisRecyclerViewId to be master")
masterView = thisRecyclerView
}
RecyclerView.SCROLL_STATE_IDLE -> if (masterView == thisRecyclerView) {
Log.d("AppLog", "resetting $thisRecyclerViewId from being master")
masterView = null
lastItemPos = Int.MIN_VALUE
}
}
}
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if ((dx == 0 && dy == 0) || (masterView != null && masterView != thisRecyclerView))
return
// Log.d("AppLog", "onScrolled:$thisRecyclerView $dx-$dy")
val currentItem = (thisRecyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
if (lastItemPos == currentItem)
return
lastItemPos = currentItem
otherRecyclerView.scrollToPosition(currentItem)
// otherRecyclerView.smoothScrollToPosition(currentItem)
Log.d("AppLog", "currentItem:" + currentItem)
}
}
}
Вопросы
-
Как я могу позволить другому RecycerView всегда иметь первый элемент, такой же, как и текущий, который находится под управлением?
-
Как изменить код для поддержки плавной прокрутки, не вызывая проблемы с тем, что другой RecyclerView является тем, который контролирует?
РЕДАКТИРОВАТЬ: после обновления здесь кода примера с разными размерами ячеек (потому что изначально, что ближе к проблеме, которую я имею, как я описал ранее), я заметил, что привязка не работает хорошо.
Вот почему я решил использовать эту библиотеку для правильной привязки:
https://github.com/DevExchanges/SnappingRecyclerview
Итак, вместо LinearSnapHelper я использую "GravitySnapHelper". Кажется, что они работают лучше, но все еще имеют проблемы с синхронизацией и касаются во время прокрутки.
EDIT: Я, наконец, исправил все проблемы синхронизации, и он отлично работает, даже если ячейки имеют разные размеры.
Все еще есть некоторые проблемы:
-
Если вы перейдете на один RecyclerView, а затем коснитесь другого, у него будет очень странное поведение прокрутки. Может прокручивать путь больше, чем нужно.
-
Прокрутка не является гладкой (при синхронизации и при переключении), поэтому она не выглядит хорошо.
-
К сожалению, из-за привязки (на самом деле это может понадобиться только для верхнего RecyclerView), это вызывает другую проблему: нижний RecyclerView может отображать последний элемент частично (снимок экрана со 100 элементами), и я могу " t прокрутите еще раз, чтобы показать его полностью:
Я даже не думаю, что нижний RecyclerView должен быть привязан, если только верхний не был затронут. К сожалению, это все, что я получил до сих пор, у меня нет проблем с синхронизацией.
Здесь новый код, после всех исправлений, которые я нашел:
class MainActivity : AppCompatActivity() {
var masterView: View? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inflater = LayoutInflater.from(this)
topReccyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder.itemView as TextView).text = position.toString()
holder.itemView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
}
override fun getItemCount(): Int = 1000
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.horizontal_cell, parent, false)) {}
}
}
bottomRecyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val baseHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50f, resources.displayMetrics).toInt()
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder.itemView as TextView).text = position.toString()
holder.itemView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
holder.itemView.layoutParams.height = baseHeight + (if (position % 3 == 0) 0 else baseHeight / (position % 3))
}
override fun getItemCount(): Int = 1000
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.vertical_cell, parent, false)) {}
}
}
// GravitySnapHelper is available from : https://github.com/DevExchanges/SnappingRecyclerview
GravitySnapHelper(Gravity.START).attachToRecyclerView(topReccyclerView)
GravitySnapHelper(Gravity.TOP).attachToRecyclerView(bottomRecyclerView)
topReccyclerView.addOnScrollListener(OnScrollListener(topReccyclerView, bottomRecyclerView))
bottomRecyclerView.addOnScrollListener(OnScrollListener(bottomRecyclerView, topReccyclerView))
}
inner class OnScrollListener(private val thisRecyclerView: RecyclerView, private val otherRecyclerView: RecyclerView) : RecyclerView.OnScrollListener() {
var lastItemPos: Int = Int.MIN_VALUE
val thisRecyclerViewId = resources.getResourceEntryName(thisRecyclerView.id)
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> if (masterView == null) {
masterView = thisRecyclerView
}
RecyclerView.SCROLL_STATE_IDLE -> if (masterView == thisRecyclerView) {
masterView = null
lastItemPos = Int.MIN_VALUE
}
}
}
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dx == 0 && dy == 0 || masterView !== null && masterView !== thisRecyclerView) {
return
}
val otherLayoutManager = otherRecyclerView.layoutManager as LinearLayoutManager
val thisLayoutManager = thisRecyclerView.layoutManager as LinearLayoutManager
val currentItem = thisLayoutManager.findFirstCompletelyVisibleItemPosition()
if (lastItemPos == currentItem) {
return
}
lastItemPos = currentItem
otherLayoutManager.scrollToPositionWithOffset(currentItem, 0)
}
}
}