在ViewPager2發佈以後,TabLayout加入了一個很是好用的中間類--TabLayoutMediator
來實現TabLayout與ViewPager2的綁定與滑動聯動效果。今天咱們就模仿TabLayoutMediator
來實現一個TabLayout與RecyclerView的錨點定位功能。效果以下圖:git
完整代碼地址:TabLayoutMediator2github
思路是很簡單的,markdown
OnTabSelectedListener
, 使RecyclerView滑動到對應位置OnScrollListener
肯定tab的選中位置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
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 } } } 複製代碼
RecyclerViewOnTabSelectedListener
-- 繼承TabLayout.OnTabSelectedListener
, 監聽TabLayout中Tab選中時,讓RecyclerView滑動到對應位置,根據RecylerView要滑動到的位置此時須要區分3種狀況ide
recyclerView.scrollToPosition
滑動到對應位置view.getTop()
與recyclerView.scrollBy(0, top)
滑動到對應位置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) } } 複製代碼
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
是模仿ViewPager2
與TabLayout
的綁定類TabLayoutMediator
實現的,使用簡單,建議你們能夠去看下原API的實現,若是有什麼問題歡迎你們留言。ui
本文使用 mdnice 排版spa