TabLayoutMediator realizes the binding sliding linkage of TabLayout+RecyclerView

background

After the release of ViewPager2, TabLayout added a very useful intermediate class - TabLayoutMediatorto realize the binding and sliding linkage effect of TabLayout and ViewPager2. Today we will imitate TabLayoutMediatorto realize the anchor point positioning function of a TabLayout and RecyclerView. The effect is as follows:

insert image description here

Complete code address: TabLayoutMediator2

general idea

The idea is very simple,

  1. When each tab is selected, OnTabSelectedListenerthe RecyclerView slides to the corresponding position by monitoring the TabLayout
  2. When the RecyclerView slides, OnScrollListenerdetermine the selected position of the tab by monitoring the RecyclerView
  3. The corresponding way between Tab and Item in RecyclerView is realized by ViewType, so that each tab is bound to the ViewType of the start Item and the end Item in RecyclerView corresponding to it.

code idea

  1. TabConfigurationStrategy-- TabLayout creates tab callback interface
 /**
  * A callback interface that must be implemented to set the text and styling of newly created
  * tabs.
  */
 interface TabConfigurationStrategy {
    
    
     /**
      * Called to configure the tab for the page at the specified position. Typically calls [ ][TabLayout.Tab.setText], but any form of styling can be applied.
      *
      * @param tab The Tab which should be configured to represent the title of the item at the given
      * position in the data set.
      * @param position The position of the item within the adapter's data set.
      * @return Adapter's first and last view type corresponding to the tab
      */
     fun onConfigureTab(tab: TabLayout.Tab, position: Int): IntArray
 }

The return value is onConfigureTabthe Array of the ViewType corresponding to the start Item and the end Item in the RecylcerView of the Tab

  1. TabLayoutOnScrollListener-- Inherited from RecyclerView.OnScrollListener(), and holds TabLayout, monitors when RecylcerView slides, and changes the selected state of Tab in TabLayout
   private class TabLayoutOnScrollListener(
       tabLayout: TabLayout
   ) : RecyclerView.OnScrollListener() {
    
    
       private var previousScrollState = 0
       private var scrollState = 0
       //是否是点击tab滚动
       var tabClickScroll: Boolean = false
       // TabLayout中Tab的选中状态
       var selectedTabPosition: Int = -1
   
       private val tabLayoutRef: WeakReference<TabLayout> = WeakReference(tabLayout)
   
       override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
    
    
           super.onScrolled(recyclerView, dx, dy)
           if (tabClickScroll) {
    
    
               return
           }
           //当前可见的第一个Item
           val currItem = recyclerView.findFirstVisibleItemPosition()
           val viewType = recyclerView.adapter?.getItemViewType(currItem) ?: -1
           //根据Item的ViewType与TabLayout中Tab的ViewType的对应情况,选中对应tab
           val tabCount = tabLayoutRef.get()?.tabCount ?: 0
           for (i in 0 until tabCount) {
    
    
               val tab = tabLayoutRef.get()?.getTabAt(i)
               val viewTypeArray = tab?.tag as? IntArray
               if (viewTypeArray?.contains(viewType) == true) {
    
    
                   val updateText =
                       scrollState != RecyclerView.SCROLL_STATE_SETTLING || previousScrollState == RecyclerView.SCROLL_STATE_DRAGGING
                   val updateIndicator =
                       !(scrollState == RecyclerView.SCROLL_STATE_SETTLING && previousScrollState == RecyclerView.SCROLL_STATE_IDLE)
                   if (selectedTabPosition != i) {
    
    
                       selectedTabPosition = i
                       // setScrollPosition不会触发TabLayout的onTabSelected回调
                       tabLayoutRef.get()?.setScrollPosition(
                           i,
                           0f,
                           updateText,
                           updateIndicator
                       )
                       break
                   }
               }
           }
       }
   
       override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
    
    
           super.onScrollStateChanged(recyclerView, newState)
           previousScrollState = scrollState
           scrollState = newState
           // 区分是手动滚动,还是调用代码滚动
           if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
    
    
               tabClickScroll = false
           }
       }
   }

  1. RecyclerViewOnTabSelectedListener-- Inheritance TabLayout.OnTabSelectedListener, when the Tab in the monitoring TabLayout is selected, let the RecyclerView slide to the corresponding position. According to the position where the RecylerView will slide to, you need to distinguish 3 situations at this time

    1. recyclerView.scrollToPositionCall directly to slide to the corresponding position before the first visible Item on the screen
    2. Between the first visible Item and the last visible Item on the screen, use view.getTop()and recyclerView.scrollBy(0, top)slide to the corresponding position
    3. After the last visible item on the screen, first use it to recyclerView.scrollToPositionmake the target item slide to be visible on the screen, and then use it recylerView.post{}, go to the second case and slide to the corresponding position

    At the same time, it is also compatible with AppBarLayout. When you need to slide to the top, the position is 0, expand the AppBar, and collapse the AppBar in other cases

private class RecyclerViewOnTabSelectedListener(
       private val recyclerView: RecyclerView,
       private val moveRecyclerViewToPosition: (recyclerViewPosition: Int, tabPosition: Int) -> Unit
   ) : OnTabSelectedListener {
    
    
       override fun onTabSelected(tab: TabLayout.Tab) {
    
    
           moveRecyclerViewToPosition(tab)
       }
   
       override fun onTabUnselected(tab: TabLayout.Tab) {
    
    
       }
   
       override fun onTabReselected(tab: TabLayout.Tab) {
    
    
           moveRecyclerViewToPosition(tab)
       }
   
       private fun moveRecyclerViewToPosition(tab: TabLayout.Tab) {
    
    
           val viewType = (tab.tag as IntArray).first()
           val adapter = recyclerView.adapter
           val itemCount = adapter?.itemCount ?: 0
           for (i in 0 until itemCount) {
    
    
               if (adapter?.getItemViewType(i) == viewType) {
    
    
                   moveRecyclerViewToPosition.invoke(i, tab.position)
                   break
               }
           }
       }
   }
   
   private fun moveRecycleViewToPosition(recyclerViewPosition: Int, tabPosition: Int) {
    
    
       onScrollListener?.tabClickScroll = true
       onScrollListener?.selectedTabPosition = tabPosition
       val firstItem: Int = recyclerView.findFirstVisibleItemPosition()
       val lastItem: Int = recyclerView.findLastVisibleItemPosition()
       when {
    
    
           // Target position before firstItem
           recyclerViewPosition <= firstItem -> {
    
    
               recyclerView.scrollToPosition(recyclerViewPosition)
           }
           // Target position in firstItem .. lastItem
           recyclerViewPosition <= lastItem -> {
    
    
               val top: Int = recyclerView.getChildAt(recyclerViewPosition - firstItem).top
               recyclerView.scrollBy(0, top)
           }
           // Target position after lastItem
           else -> {
    
    
               recyclerView.scrollToPosition(recyclerViewPosition)
               recyclerView.post {
    
    
                   moveRecycleViewToPosition(recyclerViewPosition, tabPosition)
               }
           }
       }
       // If have appBar, expand or close it
       if (recyclerViewPosition == 0) {
    
    
           appBarLayout?.setExpanded(true, false)
       } else {
    
    
           appBarLayout?.setExpanded(false, false)
       }
   }

  1. attachmethod, initialize various monitors, bind RecyclerView and TabLayout.

Instructions

It is very simple to use, just create a new one TabLayoutMediator2and call attach()it

val tabTextArrayList = arrayListOf("demo1", "demo2", "demo3")
val tabViewTypeArrayList = arrayListof(intArrayOf(1, 2), intArrayOf(7, 8), intArrayOf(9, 11))

TabLayoutMediator2(
    tabLayout = binding.layoutGoodsDetailTop.tabLayout,
    recyclerView = binding.recyclerView,
    tabCount = tabTextArrayList.size,
    appBarLayout = binding.appbar,
    autoRefresh = false,
    tabConfigurationStrategy = object : TabLayoutMediator2.TabConfigurationStrategy {
    
    
        override fun onConfigureTab(tab: TabLayout.Tab, position: Int): IntArray {
    
    
            tab.setText(tabTextArrayList[position])
            return tabViewTypeArrayList[position]
        }
    }
).apply {
    
    
    attach()
}

at last

TabLayoutMediator2It is implemented by imitating the binding class of and. It is easy to use. I suggest that you can read the implementation of the original API. If you have any questions, please leave a ViewPager2message TabLayout.TabLayoutMediator

Reprint: https://juejin.cn/post/6878160381966024718

Guess you like

Origin blog.csdn.net/gqg_guan/article/details/131813185