background

After ViewPager2 was released, TabLayout added a useful intermediate class called TabLayoutMediator to implement the binding and sliding effect of TabLayout and ViewPager2. Today we imitate TabLayoutMediator to achieve a TabLayout and RecyclerView anchor location function. The effect is shown below:

The anchor point positioning

TabLayoutMediator2

General train of thought

The idea is very simple,

  1. Each time a TAB is selected, listen on the TabLayoutOnTabSelectedListener, so that RecyclerView slides to the corresponding position
  2. In RecyclerView sliding, by listening to RecyclerViewOnScrollListenerDetermine where TAB is selected
  3. The corresponding way of Tab and RecyclerView Item is realized by using ViewType. Let each Tab bind the ViewType of the beginning Item and the end Item in RecyclerView corresponding to it.

Code thinking

  1. TabConfigurationStrategy— TabLayout Creates a 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  } Copy the code

The return value of onConfigureTab is the Array of the ViewType of the start and end items in RecylcerView corresponding to this Tab

  1. TabLayoutOnScrollListener– inheritance inRecyclerView.OnScrollListener(), and hold the TabLayout, listen to RecylcerView slide, change the TabLayout Tab selected state
   private class TabLayoutOnScrollListener(
       tabLayout: TabLayout
   ) : RecyclerView.OnScrollListener() {
       private var previousScrollState = 0
       private var scrollState = 0
 // Click TAB to scroll  var tabClickScroll: Boolean = false  // The Tab selection status in TabLayout  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  }  // The first Item currently visible  val currItem = recyclerView.findFirstVisibleItemPosition()  valviewType = recyclerView.adapter? .getItemViewType(currItem) ? : -1  // Select the corresponding Tab according to the mapping between the ViewType of the Item and the ViewType of the Tab in TabLayout  val tabCount = tabLayoutRef.get()? .tabCount ? :0  for (i in 0 until tabCount) {  val tab = tabLayoutRef.get()? .getTabAt(i) valviewTypeArray = tab? .tagas? 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 does not trigger the TabLayout onTabSelected callback  tabLayoutRef.get()? .setScrollPosition( i,  0f. updateText,  updateIndicator  )  break  }  }  }  }   override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {  super.onScrollStateChanged(recyclerView, newState)  previousScrollState = scrollState  scrollState = newState  // Distinguish between manual scrolling and code scrolling  if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {  tabClickScroll = false  }  }  } Copy the code
  1. RecyclerViewOnTabSelectedListener – inheritance TabLayout OnTabSelectedListener, listening TabLayout Tab in the selected, let RecyclerView slide to the corresponding position, Depending on where you want to slide your RecylerView, you need to distinguish between three cases

    1. Called directly before the first Item is visible on the screenrecyclerView.scrollToPositionSlide to position
    2. Between the first visible Item on the screen and the last visible Itemview.getTop()withrecyclerView.scrollBy(0, top)Slide to position
    3. Used first after the last Item visible on the screenrecyclerView.scrollToPositionSlide the target Item onto the screen before using itrecylerView.post{}, go to the second case and slide to the corresponding position

    It is also compatible with AppBarLayout. When you need to slide to the top, i.e. position is 0, expand the AppBar, and collapse the AppBar otherwise

   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  valitemCount = 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)  }  } Copy the code
  1. attachMethod, initialize a variety of monitoring, binding RecyclerView and TabLayout.

Method of use

It’s as simple as creating a new TabLayoutMediator2 and calling Attach ()

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() } Copy the code

The last

TabLayoutMediator2 is modeled after ViewPager2 and TabLayout binding class TabLayoutMediator implementation, easy to use, I suggested you can look at the original API implementation, if you have any questions welcome everyone to leave a message.

This article is formatted using MDNICE