Почему нельзя использовать RecyclerView в NestedScrollView и как это исправить?

от автора

Чтобы ответить на этот вопрос, сначала нужно понять как работает RecyclerView. У Recycler есть ViewHolder’ы, часть из них видны, а другие два пользователь не видит. Один расположен сверху, второй снизу. При прокрутки списка, старые ViewHolder’ы меняются на новые, потому что в адаптере срабатывает onBindViewHolder.

Попробуем реализовать экран со списком цифр.

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout      xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"     android:layout_width="match_parent"     android:layout_height="match_parent">      <androidx.recyclerview.widget.RecyclerView         android:id="@+id/recyclerView"         android:layout_width="match_parent"         android:layout_height="match_parent"         app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />  </androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:layout_width="match_parent"     android:layout_height="wrap_content">      <TextView         android:id="@+id/tvName"         android:layout_width="match_parent"         android:layout_height="wrap_content"         android:padding="10dp"         android:layout_gravity="center_horizontal"/>      <View         android:layout_width="match_parent"         android:layout_height="1dp"         android:background="@color/gray"/>  </FrameLayout>

Также нужно сделать адаптер.

class CustomAdapter() : RecyclerView.Adapter<CustomAdapter.CustomViewHolder>() {     private var data: List<Int> = listOf()       inner class CustomViewHolder(private val binding: ItemRvBinding) :         RecyclerView.ViewHolder(binding.root) {         fun onBind(value: Int) {             binding.tvName.text = value.toString()         }     }      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {         return CustomViewHolder(             ItemRvBinding.inflate(                 LayoutInflater.from(parent.context),                 parent,                 false             )         )     }      override fun getItemCount(): Int {         return data.size     }      override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {         holder.onBind(data[position])     }      fun setList(list: List<Int>) {         data = list         notifyDataSetChanged()     } }

setList нельзя так использовать, я написал не оптимальный способ для простоты понимая. В настоящих проектах нужно пользоваться DiffUtil.

class MainActivity : AppCompatActivity() {     private lateinit var binding: ActivityMainBinding      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         binding = ActivityMainBinding.inflate(layoutInflater)         setContentView(binding.root)          val adapter = CustomAdapter()         binding.recyclerView.adapter = adapter          binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {             override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {                 Log.d(                     "COUNT_VISIBLE_ITEM",                     binding.recyclerView.layoutManager?.childCount.toString()                 )                 super.onScrollStateChanged(recyclerView, newState)             }              override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {                 super.onScrolled(recyclerView, dx, dy)             }         })          val list = mutableListOf<Int>()         for (i in 1..1000) {             list.add(i)         }          adapter.setList(list)     } }

На recyclerView я поставил addOnScrollListener, чтобы он выводил количество ViewHolder’ов при каждой прокрутке.

Запускаем проект, и смотрим логи. Число ViewHolder’ов варьируется от 22 до 23.

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

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout      xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"     android:layout_width="match_parent"     android:layout_height="match_parent">      <androidx.core.widget.NestedScrollView         android:layout_width="match_parent"         android:layout_height="match_parent"         android:orientation="vertical">           <LinearLayout             android:layout_width="match_parent"             android:layout_height="match_parent"             android:orientation="vertical">              <ImageView                 android:layout_width="200dp"                 android:layout_height="200dp"                 android:layout_gravity="center_horizontal"                 android:layout_marginTop="8dp"                 android:src="@drawable/ic_github" />              <TextView                 android:layout_width="match_parent"                 android:layout_height="wrap_content"                 android:layout_marginTop="8dp"                 android:gravity="center_horizontal"                 android:text="@string/name"                 android:textColor="@color/black"                 android:textSize="20sp"                 android:textStyle="bold" />              <androidx.recyclerview.widget.RecyclerView                 android:id="@+id/recyclerView"                 android:layout_width="match_parent"                 android:layout_height="match_parent"                 android:layout_marginTop="16dp"                 app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />           </LinearLayout>     </androidx.core.widget.NestedScrollView> </androidx.constraintlayout.widget.ConstraintLayout>

Получается что‑то вот такое. Но зайдя в логи, можно увидеть ужасающую надпись.
COUNT_VISIBLE_ITEM habr_tests D 1000 

Так как RecyclerView находиться в NestedScrollView, его размер по вертикали растягивается на весь экран. Из‑за этого отрисовываются сразу все ViewHolder’ы, и телефону накладно все это обрабатывать. Можно увидеть лаги, если листать такой список. Так делать нельзя. Но как это исправить?

Есть два варианта, как можно сделать.

  • CollapsingToolbarLayout

  • ViewType

CollapsingToolbarLayout

Изменим activity_main.xml.

<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"     android:layout_width="match_parent"     android:layout_height="match_parent">      <com.google.android.material.appbar.AppBarLayout         android:layout_width="match_parent"         android:layout_height="wrap_content">          <com.google.android.material.appbar.CollapsingToolbarLayout             android:layout_width="match_parent"             android:layout_height="match_parent"             app:layout_scrollFlags="scroll|exitUntilCollapsed">              <LinearLayout                 android:layout_width="match_parent"                 android:layout_height="match_parent"                 android:orientation="vertical"                 app:layout_collapseMode="pin">                  <ImageView                     android:layout_width="200dp"                     android:layout_height="200dp"                     android:layout_gravity="center_horizontal"                     android:layout_marginTop="8dp"                     android:src="@drawable/ic_github" />                  <TextView                     android:layout_width="match_parent"                     android:layout_height="wrap_content"                     android:layout_marginTop="8dp"                     android:gravity="center_horizontal"                     android:text="@string/name"                     android:textColor="@color/black"                     android:textSize="20sp"                     android:textStyle="bold" />             </LinearLayout>         </com.google.android.material.appbar.CollapsingToolbarLayout>     </com.google.android.material.appbar.AppBarLayout>      <androidx.recyclerview.widget.RecyclerView         android:id="@+id/recyclerView"         android:layout_width="match_parent"         android:layout_height="match_parent"         app:layout_behavior="@string/appbar_scrolling_view_behavior"         android:paddingTop="16dp"         android:clipToPadding="false"         app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />   </androidx.coordinatorlayout.widget.CoordinatorLayout>

В RecyclerView мы должны установить layout_behavior. Это нужно,чтобы recyclerView был под AppBarLayout’ом. Также прописываем layout_scrollFlags и layout_collapseMode. Далее смотрим в логи и радуемся жизни, ведь теперь число ViewHolder’ов вернулась к 22–23.

ViewType

Идея этого подхода заключается в создании нового ViewType в адаптере.

Создадим item_tittle.xml.

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:layout_width="match_parent"     android:orientation="vertical"     android:layout_height="wrap_content">      <ImageView         android:layout_width="200dp"         android:layout_height="200dp"         android:layout_gravity="center_horizontal"         android:layout_marginTop="8dp"         android:src="@drawable/ic_github" />      <TextView         android:id="@+id/tvTittle"         android:layout_width="match_parent"         android:layout_height="wrap_content"         android:layout_marginTop="8dp"         android:gravity="center_horizontal"         android:text="@string/name"         android:textColor="@color/black"         android:textSize="20sp"         android:textStyle="bold" />  </LinearLayout>

Поменяем CustomAdapter.

class CustomAdapter() : RecyclerView.Adapter<CustomAdapter.CustomViewHolder>() {     private var data: List<Any> = listOf()      abstract class CustomViewHolder(view: View): RecyclerView.ViewHolder(view){         open fun onBind(position: Int){}     }      inner class ValueCustomViewHolder(private val binding: ItemRvBinding): CustomViewHolder(binding.root) {         override fun onBind(position: Int) {             val value = data[position] as Int             binding.tvName.text = value.toString()         }     }      inner class TittleCustomViewHolder(private val binding: ItemTittleBinding): CustomViewHolder(binding.root){         override fun onBind(position: Int) {             val value = data[position] as String             binding.tvTittle.text = value         }     }      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {         return when(viewType){             TITTLE_VIEW_TYPE -> TittleCustomViewHolder(ItemTittleBinding.inflate(LayoutInflater.from(parent.context), parent, false))             VALUE_VIEW_TYPE -> ValueCustomViewHolder(ItemRvBinding.inflate(LayoutInflater.from(parent.context), parent, false))             else -> throw IllegalArgumentException("Invalid viewType: $viewType")         }     }      override fun getItemViewType(position: Int): Int {         return when(data[position]){             is Int -> VALUE_VIEW_TYPE             is String -> TITTLE_VIEW_TYPE             else -> throw IllegalArgumentException("Invalid data: ${data[position]}")         }     }      override fun getItemCount(): Int {         return data.size     }      override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {         when(holder){             is ValueCustomViewHolder -> {                 holder.onBind(position)             }             is TittleCustomViewHolder ->{                 holder.onBind(position)             }         }     }      fun setList(list: List<Any>) {         data = list         notifyDataSetChanged()     }      companion object{         const val TITTLE_VIEW_TYPE = 0         const val VALUE_VIEW_TYPE = 1     } }

Создадим абстрактный класс CustomViewHolder и в нем функцию onBind. Далее наследуемся от него и создаем ViewHolder под Заголовок и числа. В MainActivity добавим строчку перед инициализацией list.

list.add(getString(R.string.name))

Теперь можем запустить, смотрим логи число ViewHolder’ов вернулась к 22–23.

Заключение

Таким образом, я показал ,что можно обойтись без NestedScrollView совместно с RecyclerView. Для это лучше подходит использование CollapsingToolBar или ViewType. Их нужно использовать в зависимости от ситуации, в данном случаи я бы лучше использовал CollapsingToolBar.


ссылка на оригинал статьи https://habr.com/ru/articles/897894/