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

相關文章
相關標籤/搜索