background
After the release of ViewPager2, TabLayout added a very useful intermediate class - TabLayoutMediator
to realize the binding and sliding linkage effect of TabLayout and ViewPager2. Today we will imitate TabLayoutMediator
to realize the anchor point positioning function of a TabLayout and RecyclerView. The effect is as follows:
Complete code address: TabLayoutMediator2
general idea
The idea is very simple,
- When each tab is selected,
OnTabSelectedListener
the RecyclerView slides to the corresponding position by monitoring the TabLayout - When the RecyclerView slides,
OnScrollListener
determine the selected position of the tab by monitoring the RecyclerView - 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
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 onConfigureTab
the Array of the ViewType corresponding to the start Item and the end Item in the RecylcerView of the Tab
TabLayoutOnScrollListener
-- Inherited fromRecyclerView.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
}
}
}
RecyclerViewOnTabSelectedListener
-- InheritanceTabLayout.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 timerecyclerView.scrollToPosition
Call directly to slide to the corresponding position before the first visible Item on the screen- Between the first visible Item and the last visible Item on the screen, use
view.getTop()
andrecyclerView.scrollBy(0, top)
slide to the corresponding position - After the last visible item on the screen, first use it to
recyclerView.scrollToPosition
make the target item slide to be visible on the screen, and then use itrecylerView.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)
}
}
attach
method, initialize various monitors, bind RecyclerView and TabLayout.
Instructions
It is very simple to use, just create a new one TabLayoutMediator2
and 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
TabLayoutMediator2
It 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 ViewPager2
message TabLayout
.TabLayoutMediator
Reprint: https://juejin.cn/post/6878160381966024718