如何将 TabLayout 与 Recyclerview 同步?

How to sync TabLayout with Recyclerview?

我有一个 TabLayoutRecyclerview,因此当单击选项卡时 Recyclerview 会滚动到特定位置。 我也想要相反的过程,以便当 Recyclerview 滚动到特定位置时突出显示特定选项卡。

例如:如果 TabLayout 中有 4 个选项卡,当 Recyclerview 滚动到第 5 个位置(项目可见且低于 TabLayout)时,应突出显示第 3 个选项卡。

此处,当 'How it works' 出现在 TabLayout 下方时,应突出显示标签 'How it works'。

试试这个

按照此步骤操作

  1. ScrollListener 添加到您的 RecyclerView
  2. 比找到您的 RecyclerView
  3. 的第一个可见项目
  4. 根据您 RecyclerView
  5. 的位置在 TabLayout 中设置 select 选项卡

示例代码

    myRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);

            int itemPosition=linearLayoutManager.findFirstCompletelyVisibleItemPosition();

            if(itemPosition==0){ //  item position of uses
                TabLayout.Tab tab = myTabLayout.getTabAt(Index);
                tab.select();
            }else if(itemPosition==1){//  item position of side effects 
                TabLayout.Tab tab = myTabLayout.getTabAt(Index);
                tab.select();
            }else if(itemPosition==2){//  item position of how it works
                TabLayout.Tab tab = myTabLayout.getTabAt(Index);
                tab.select();
            }else if(itemPosition==3){//  item position of precaution 
                TabLayout.Tab tab = myTabLayout.getTabAt(Index);
                tab.select();
            }
        }
    });

编辑

public class MyActivity extends AppCompatActivity {


    RecyclerView myRecyclerView;
    TabLayout myTabLayout;
    LinearLayoutManager linearLayoutManager;
    ArrayList<String> arrayList = new ArrayList<>();
    DataAdapter adapter;
    private boolean isUserScrolling = false;
    private boolean isListGoingUp = true;




    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my);

        myTabLayout = findViewById(R.id.myTabLayout);



        myRecyclerView = findViewById(R.id.myRecyclerView);
        linearLayoutManager = new LinearLayoutManager(this);
        myRecyclerView.setLayoutManager(linearLayoutManager);
        myRecyclerView.setHasFixedSize(true);

        for (int i = 0; i < 120; i++) {
            arrayList.add("Item " + i);
        }

        adapter= new DataAdapter(this,arrayList);
        myRecyclerView.setAdapter(adapter);

        myTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                isUserScrolling = false ;
                int position = tab.getPosition();
                if(position==0){
                    myRecyclerView.smoothScrollToPosition(0);
                }else if(position==1){
                    myRecyclerView.smoothScrollToPosition(30);
                }else if(position==2){
                    myRecyclerView.smoothScrollToPosition(60);
                }else if(position==3){
                    myRecyclerView.smoothScrollToPosition(90);
                }
            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {

            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {

            }
        });
        myRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
                    isUserScrolling = true;
                    if (isListGoingUp) {
                        //my recycler view is actually inverted so I have to write this condition instead
                        if (linearLayoutManager.findLastCompletelyVisibleItemPosition() + 1 == arrayList.size()) {
                            Handler handler = new Handler();
                            handler.postDelayed(new Runnable() {
                                @Override
                                public void run() {
                                    if (isListGoingUp) {
                                        if (linearLayoutManager.findLastCompletelyVisibleItemPosition() + 1 == arrayList.size()) {
                                            Toast.makeText(MyActivity.this, "exeute something", Toast.LENGTH_SHORT).show();
                                        }
                                    }
                                }
                            }, 50);
                            //waiting for 50ms because when scrolling down from top, the variable isListGoingUp is still true until the onScrolled method is executed
                        }
                    }
                }

            }
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                int itemPosition = linearLayoutManager.findFirstVisibleItemPosition();


                if(isUserScrolling){
                    if (itemPosition == 0) { //  item position of uses
                        TabLayout.Tab tab = myTabLayout.getTabAt(0);
                        tab.select();
                    } else if (itemPosition == 30) {//  item position of side effects
                        TabLayout.Tab tab = myTabLayout.getTabAt(1);
                        tab.select();
                    } else if (itemPosition == 60) {//  item position of how it works
                        TabLayout.Tab tab = myTabLayout.getTabAt(2);
                        tab.select();
                    } else if (itemPosition == 90) {//  item position of precaution
                        TabLayout.Tab tab = myTabLayout.getTabAt(3);
                        tab.select();
                    }
                }



            }
        });


    }


}
private fun syncTabWithRecyclerView() {

        // Move recylerview to the position selected by user
        menutablayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
            override fun onTabSelected(tab: TabLayout.Tab) {
                if (!isUserScrolling) {
                    val position = tab.position
                    linearLayoutManager.scrollToPositionWithOffset(position, 0)
                }


            }

            override fun onTabUnselected(tab: TabLayout.Tab) {

            }

            override fun onTabReselected(tab: TabLayout.Tab) {
            }
        })

        // Detect recyclerview position and select tab respectively.
        menuRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {

            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
                    isUserScrolling = true
                }  else if (newState == RecyclerView.SCROLL_STATE_IDLE) 
                    isUserScrolling = false
                }
            }

            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                if (isUserScrolling) {
                    var itemPosition = 0
                    if (dy > 0) {
                      // scrolling up
                       itemPosition = linearLayoutManager.findLastVisibleItemPosition()
                    } else {
                      // scrolling down
                       itemPosition = linearLayoutManager.findFirstVisibleItemPosition()
                    }
                    val tab = menutablayout.getTabAt(itemPosition)
                    tab?.select()
                }
            }
        })
    }

我想你甚至不需要这些标志,它足以覆盖 RecyclerView 的 onScrolled 和 select 选项卡并滚动到选项卡 selected 并且该选项卡尚未编辑时的位置select编辑:

override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
     val llm = recyclerView.layoutManager as LinearLayoutManager

     // depending on sections'heights one may want to add more logic 
     // on how to determine which section to scroll to
     val firstCompletePos = llm.findFirstCompletelyVisibleItemPosition()

     if (firstCompletePos != tabLayout.selectedTabPosition)
         tabLayout.getTabAt(firstCompletePos)?.select()
}

然后我有一个 TextView,它被设置为 tabLayout 的 customView:

tabLayout.addTab(newTab().also { tab ->
         tab.customView = AppCompatTextView(context).apply {
             // set layout params match_parent, so the entire section is clickable
             // set style, gravity, text etc.
             setOnClickListener { 
                tabLayout.selectTab(tab)

                recyclerView.apply {
                    val scrollTo = tabLayout.selectedTabPosition
                    smoothScrollToPosition(scrollTo)
                }
             }
          }
})

通过此设置,您拥有:

  1. Tab select 当用户滚动和滑动时
  2. 当用户单击选项卡时,RecyclerView 会滚动。

我使用了其他答案中的信息,但这里有一些代码遗漏,这使得它不完整且无法正常工作。我的解决方案 100% 无延迟。最后你可能会看到完整的屏幕照片。

这是里面的代码Fragment:

private val layoutManager get() = recyclerView?.layoutManager as? LinearLayoutManager

/**
 * [SmoothScroller] need for smooth scrolling inside [tabListener] of [recyclerView] 
 * to top border of [RecyclerView.ViewHolder].
 */
private val smoothScroller: SmoothScroller by lazy {
    object : LinearSmoothScroller(context) {
        override fun getVerticalSnapPreference(): Int = SNAP_TO_START
    }
}

/**
 * Variable for prevent calling of [RecyclerView.OnScrollListener.onScrolled]
 * inside [scrollListener], when user click on [TabLayout.Tab] and 
 * [tabListener] was called.
 *
 * Fake calls happens because of [tabListener] have smooth scrolling to position,
 * and when [scrollListener] catch scrolling and call [TabLayout.Tab.select].
 */
private var isTabClicked = false

/**
 * Variable for prevent calling of [TabLayout.OnTabSelectedListener.onTabSelected]
 * inside [tabListener], when user scroll list and function 
 * [RecyclerView.OnScrollListener.onScrolled] was called inside [scrollListener].
 *
 * Fake calls happens because [scrollListener] contains call of [TabLayout.Tab.select],
 * which in turn calling click handling inside [tabListener].
 */
private var isScrollSelect = false

private val scrollListener = object : RecyclerView.OnScrollListener() {

    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        /**
         * Reset [isTabClicked] key when user start scroll list.
         */
        if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
            isTabClicked = false
        }
    }

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        /**
         * Prevent scroll handling after tab click (see inside [tabListener]).
         */
        if (isTabClicked) return

        val commonIndex = commonIndex ?: return
        val karaokeIndex = karaokeIndex ?: return
        val socialIndex = socialIndex ?: return
        val reviewIndex = reviewIndex ?: return
        val addIndex = addIndex ?: return

        when (layoutManager?.findFirstVisibleItemPosition() ?: return) {
            in commonIndex until karaokeIndex -> selectTab(TabIndex.COMMON)
            in karaokeIndex until socialIndex -> selectTab(TabIndex.KARAOKE)
            in socialIndex until reviewIndex -> {
                /**
                 * In case if [reviewIndex] can't reach top of the list,
                 * to become first visible item. Need check [addIndex] 
                 * (last element of list) completely visible or not.
                 */
                if (layoutManager?.findLastCompletelyVisibleItemPosition() != addIndex) {
                    selectTab(TabIndex.CONTACTS)
                } else {
                    selectTab(TabIndex.REVIEWS)
                }
            }
            in reviewIndex until addIndex -> selectTab(TabIndex.REVIEWS)
        }
    }

    /**
     * It's very important to skip cases when [TabLayout.Tab] is checked like current,
     * otherwise [tabLayout] will terribly lagging on [recyclerView] scroll.
     */
    private fun selectTab(@TabIndex index: Int) {
        val tab = tabLayout?.getTabAt(index) ?: return
        if (!tab.isSelected) {
            recyclerView?.post {
                isScrollSelect = true
                tab.select()
            }
        }
    }
}

private val tabListener = object : TabLayout.OnTabSelectedListener {
    override fun onTabSelected(tab: TabLayout.Tab?) = scrollToPosition(tab)

    override fun onTabUnselected(tab: TabLayout.Tab?) = Unit

    /*
     * If user click on tab again.
     */
    override fun onTabReselected(tab: TabLayout.Tab?) = scrollToPosition(tab)

    private fun scrollToPosition(tab: TabLayout.Tab?) {
        /**
         * Prevent scroll to position calling from [scrollListener].
         */
        if (isScrollSelect) {
            isScrollSelect = false
            return
        }

        val position = when (tab?.position) {
            TabIndex.COMMON -> commonIndex
            TabIndex.KARAOKE -> karaokeIndex
            TabIndex.CONTACTS -> socialIndex
            TabIndex.REVIEWS -> reviewIndex
            else -> null
        }

        if (position != null) {
            isTabClicked = true

            smoothScroller.targetPosition = position
            layoutManager?.startSmoothScroll(smoothScroller)
        }
    }
}

private val commonIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.Info }
private val karaokeIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.Karaoke }
private val socialIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.Social }
private val reviewIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.ReviewHeader }
private val addIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.AddReview }

分机:

private const val ND_INDEX = -1

fun <T> List<T>.validIndexOfFirst(predicate: (T) -> Boolean): Int? {
    return indexOfFirst(predicate).takeIf { it != ND_INDEX }
}

TabIndex class 按位置获取标签:

@IntDef(TabIndex.COMMON, TabIndex.KARAOKE, TabIndex.CONTACTS, TabIndex.REVIEWS)
private annotation class TabIndex {
    companion object {
        const val COMMON = 0
        const val KARAOKE = 1
        const val CONTACTS = 2
        const val REVIEWS = 3
    }
}

这就是我的样子 ClubScreenItem:

sealed class ClubScreenItem {
    class Info(val data: ClubItem): ClubScreenItem()
    ...
    class Karaoke(...): ClubScreenItem()
    class Social(...): ClubScreenItem()
    ...
    class ReviewHeader(...): ClubScreenItem()
    ...
    object AddReview : ClubScreenItem()
}

这是屏幕的样子:

试试这个,

简单步骤:

  1. 检测 RecyclerView 滚动状态
  2. 使用findFirstVisibleItemPosition()到return第一个可见视图的适配器位置
  3. 根据 RecyclerView 项目位置更改选项卡
  4. 完成
 private fun syncTabWithRecyclerView() {
        var isUserScrolling = false
        val layoutManager = binding.recyclerViewGroup.layoutManager as LinearLayoutManager

        val tabListener = object : TabLayout.OnTabSelectedListener {
                override fun onTabSelected(tab: TabLayout.Tab?) {
                    val tabPosition = tab?.position
                    if (tabPosition != null) {
                        viewModel.setTabPosition(tabPosition)
                        
                        // prevent RecyclerView to snap to its item start position while user scrolling,
                        // idk how to explain this XD
                        if (!isUserScrolling){
                            layoutManager.scrollToPositionWithOffset(tabPosition, 0)
                        }
                    }
                }
                override fun onTabUnselected(tab: TabLayout.Tab?) {}
                override fun onTabReselected(tab: TabLayout.Tab?) {}
            }

        binding.tabLayout.addOnTabSelectedListener(tabListener)

        // Detect recyclerview scroll state
        val onScrollListener = object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
                    isUserScrolling = true
                } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    isUserScrolling = false
                }
            }

            // this just represent my tab name using enum class ,
            // and ordinal is just the index of its position in enum
            val hardcase3D = CaseType.HARDCASE_3D.ordinal
            val softcaseBlackmatte = CaseType.SOFTCASE_BLACKMATTE.ordinal
            val softcaseTransparent = CaseType.SOFTCASE_TRANSPARENT.ordinal


            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                if (isUserScrolling) {
                    when (layoutManager.findFirstVisibleItemPosition()) {
                        in hardcase3D until softcaseBlackmatte -> {
                            viewModel.setTabPosition(hardcase3D)
                        }
                        in softcaseBlackmatte until softcaseTransparent -> {
                            viewModel.setTabPosition(softcaseBlackmatte)
                        }
                        softcaseTransparent -> {
                            viewModel.setTabPosition(softcaseTransparent)
                        }
                    }
                }
            }
        }

        binding.recyclerViewGroup.addOnScrollListener(onScrollListener)
    } 

viewModel ,你可以简单地使用 liveData 如果你想要

private var _tabPosition = MutableStateFlow(CaseType.HARDCASE_3D)
    val tabPostition : StateFlow<CaseType>
        get() = _tabPosition

 fun setTabPosition(position: Int){
        _tabPosition.value = CaseType.values()[position]
    }

观察者

lifecycleScope.launch(Dispatchers.Default) {
            viewModel.tabPostition.collect { caseType ->
                val positionIndex = CaseType.values().indexOf(caseType)
                handleSelectedTab(positionIndex)
            }
        }

和 handleSelectedTab

private fun handleSelectedTab(index: Int) {
       val tab = binding.tabLayout.getTabAt(index)
       tab?.select()
    }

枚举

enum class CaseType(val caseTypeName:String) {
    HARDCASE_3D("Hardcase 3D"),
    SOFTCASE_BLACKMATTE("Softcase Blackmatte"),
    SOFTCASE_TRANSPARENT("Softcase Transparent")
}