[译] RecyclerView正确使用姿势

4,378 阅读3分钟

这篇文章主要讨论解决了俩个问题,外层 RecyclerView 垂直滚动时嵌套的横向 RecyclerView 滑动位置的丢失以及水平方向滑动与垂直滑动的冲突解决。

So,Here we go !!!

构建一个示例程序

中间省略一大堆翻译,就是构建了俩个适配器,直奔实现效果(直接搬原作者图了)

在垂直滑动的 RecyclerView 嵌套了几个横向滑动的 RecyclerView,通过最新推出的 ConcatAdapter 加以实现。需要了解 ConcatAdapter 更多细节的可以看一下这篇文章 给 Adapter 做 “加法” —— 实战 MergeAdapter ,该文推出时为测试版,正式版已经更名为 ConcatAdapter ,不影响理解和使用。

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var concatAdapter: ConcatAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
        initViews()
    }

    private fun initViews() {
        //create a populated list of sections
        val sections = DataSource.createSections(numberOfSections = 50, itemsPerSection = 25)

        //create an instance of ConcatAdapter
        concatAdapter = ConcatAdapter()

        //create AnimalSectionAdapter for the sections and add to ConcatAdapter
        val sectionAdapter = AnimalSectionAdapter(sections)
        concatAdapter.addAdapter(sectionAdapter)

        //setup the recycler
        val linearLayoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
        binding.recyclerView.run {
            layoutManager = linearLayoutManager
            adapter = concatAdapter
        }
    }
}

可以简单看一下构建的过程,没什么好说的。

出现的问题

滑动位置的丢失

当垂直滚动时,顶部行的水平滚动位置将丢失。

通过上图可以直观的看到滑动过的横向 RecyclerView 在重新显示的时候滑动位置的丢失。为了解决这个问题,分为俩步走。在回收的时候保存滑动的位置,以及重新绑定时恢复滑动的位置。

class AnimalSectionAdapter(
//...
) {
	private val scrollStates: MutableMap<String, Parcelable?> = mutableMapOf()

	private fun getSectionID(position: Int): String {
        	return items[position].id
    	}

	override fun onViewRecycled(holder: BaseViewHolder<AnimalSection>) {
        	super.onViewRecycled(holder)

        	//save horizontal scroll state
        	val key = getSectionID(holder.layoutPosition)
        	scrollStates[key] =
           		holder.itemView.findViewById<RecyclerView>(R.id.titledSectionRecycler).layoutManager?.onSaveInstanceState()
    	}

    	override fun onBindViewHolder(
   	 //...
   	 ) {
		 //restore horizontal scroll state
        	val key = getSectionID(viewHolder.layoutPosition)
       		val state = scrollStates[key]
		if (state != null) {
 			titledSectionRecycler.layoutManager?.onRestoreInstanceState(state)
		} else {
    			titledSectionRecycler.layoutManager?.scrollToPosition(0)
		}
     }
      //...
}

在 Adapter 中创建一个 MutableMap 来持久化状态,重写 onViewRecycled 方法来保存对应的数据,key 为位置,value 为 layoutManager 调用 onSaveInstanceState() 方法后生成的序列化对象。在 onBindViewHolder 方法中,绑定时对位置进行判断,如果 state 不为空,就恢复位置,否则滑动到最前面的位置。

水平方向与垂直方向滑动的冲突解决

原作者直接使用了他人的解决方案,可以到原文中查看。定义了一个 RecyclerView 的扩展函数,添加了触摸和滑动的处理,从而解决了这个问题。

fun RecyclerView.enforceSingleScrollDirection() {
    val enforcer = SingleScrollDirectionEnforcer()
    addOnItemTouchListener(enforcer)
    addOnScrollListener(enforcer)
}

其他的解决方案

回收池

当外层的 RecyclerView 垂直滚动时,嵌套的横向 RecyclerView 会将视图重新加载一边,这是因为每个嵌套的 RecyclerView 拥有各自的 View Pool。可以通过给相同视图类型的 RecyclerView 设置一个共享的 View Pool,这样可以减少 View 的创建,提高了垂直方向滚动的性能。

在这个示例项目中,显然这样做是可以的。

class AnimalSectionAdapter(
//...
) {
    //create an instance of ViewPool
    private val viewPool = RecyclerView.RecycledViewPool()

    //and set it to each nested recycler when binding
    override fun onBindViewHolder(
    //...
    ) {
      //...
        titledSectionRecycler?.run {
            //right here
            this.setRecycledViewPool(viewPool)
            this.layoutManager = layoutManager
            this.adapter = AnimalAdapter(item.animals)
        }
      //...
    }
}

设置预加载的数量

可以通过嵌套的 RecyclerView 的 LinearLayoutManager ,调用 setInitialPrefetechItemCount() 方法来预设可能会显示的可见数量。在垂直滑动的时候,外层 RecyclerView 会要求内层 RecyclerView 进行预绑定,但是内层 RecyclerView 并不知道应该预加载多少个 Item,直到该内层 RecyclerView 可见的时候,其他的非预加载 Item 才会被加载(默认情况下只有俩个 Item 会被预加载),这样会导致性能问题。

可以通过下面的方式来解决这个问题:

class AnimalSectionAdapter(
//...
) {
    override fun onBindViewHolder(
    //... 
    ) {
      //...
      val layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false)

      //right here
      layoutManager.initialPrefetchItemCount = 4 //estimated number of visible items
      
      val titledSectionRecycler = itemView.findViewById<RecyclerView>(R.id.titledSectionRecycler)
      titledSectionRecycler?.run {
            this.layoutManager = layoutManager
            //...
      }
      //...
    }
}

翻译的水平有限 ,敬请谅解!