TabLayoutMediator2 -- 实现TabLayout+RecyclerView的锚点定位

背景

在ViewPager2发布以后,TabLayout加入了一个很是好用的中间类--TabLayoutMediator来实现TabLayout与ViewPager2的绑定与滑动联动效果。今天咱们就模仿TabLayoutMediator来实现一个TabLayout与RecyclerView的锚点定位功能。效果以下图:git

锚点定位
锚点定位

完整代码地址:TabLayoutMediator2github

大体思路

思路是很简单的,markdown

  1. 在每次tab选中的时候, 经过监听TabLayout的OnTabSelectedListener, 使RecyclerView滑动到对应位置
  2. 在RecyclerView滑动的时候,经过监听RecyclerView的OnScrollListener肯定tab的选中位置
  3. Tab与RecyclerView中的Item的对应方式使用ViewType来实现,让每一个tab绑定它所对应的RecyclerView中起始Item与末尾Item的ViewType。

代码思路

  1. TabConfigurationStrategy -- TabLayout建立tab的回调接口
/**  * 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  } 复制代码

其中onConfigureTab的返回值即为该Tab对应RecylcerView中起始Item与末尾Item的ViewType的Arrayapp

  1. TabLayoutOnScrollListener -- 继承于RecyclerView.OnScrollListener(),并持有TabLayout,监听RecylcerView滑动时, 改变TabLayout中Tab的选中状态
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 -- 继承TabLayout.OnTabSelectedListener, 监听TabLayout中Tab选中时,让RecyclerView滑动到对应位置,根据RecylerView要滑动到的位置此时须要区分3种状况ide

    1. 在屏幕中第一个可见Item以前,直接调用recyclerView.scrollToPosition滑动到对应位置
    2. 在屏幕第一个可见Item和最后一个可见Item之间,使用view.getTop()recyclerView.scrollBy(0, top)滑动到对应位置
    3. 在屏幕最后一个可见Item以后,先使用recyclerView.scrollToPosition让目标Item滑动到屏幕中可见,再使用recylerView.post{}, 走第二种状况,滑动到对应位置

    同时也兼容AppBarLayout,当须要滑动到最上方即position为0,展开AppBar, 其余状况折叠AppBaroop

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. attach方法,初始化各类监听,绑定RecyclerView与TabLayout。

使用方法

使用起来很是简单,只须要新建一个TabLayoutMediator2并调用attach()就行了post

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() } 复制代码

最后

TabLayoutMediator2是模仿ViewPager2TabLayout的绑定类TabLayoutMediator实现的,使用简单,建议你们能够去看下原API的实现,若是有什么问题欢迎你们留言。ui

本文使用 mdnice 排版spa

相关文章
相关标签/搜索