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:
TabLayoutMediator2
General train of thought
The idea is very simple,
- Each time a TAB is selected, listen on the TabLayout
OnTabSelectedListener
, so that RecyclerView slides to the corresponding position - In RecyclerView sliding, by listening to RecyclerView
OnScrollListener
Determine where TAB is selected - 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
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
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
-
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
- Called directly before the first Item is visible on the screen
recyclerView.scrollToPosition
Slide to position - Between the first visible Item on the screen and the last visible Item
view.getTop()
withrecyclerView.scrollBy(0, top)
Slide to position - Used first after the last Item visible on the screen
recyclerView.scrollToPosition
Slide 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
- Called directly before the first Item is visible on the screen
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
attach
Method, 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