如何将TabLayout与Recyclerview同步?

11
我有一个带有Recyclerview的TabLayout,当点击选项卡时,Recyclerview会滚动到特定位置。我也想要相反的过程,即当Recyclerview滚动到特定位置时,特定的选项卡会被突出显示。
例如:如果TabLayout中有4个选项卡,并且当Recyclerview滚动到第5个位置(项目可见并在TabLayout下方)时,则应突出显示第3个选项卡。

enter image description here

TabLayout下方出现“它是如何工作的”时,应突出显示“它是如何工作的”选项卡。
5个回答

18

尝试这个

按照以下步骤执行

  1. ScrollListener 添加到您的 RecyclerView
  2. 找到您的 RecyclerView 中第一个可见项
  3. 根据您的 RecyclerView 的位置选择 TabLayout 中的选项卡

示例代码

    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();
                    }
                }



            }
        });


    }


}

这个实际上是有效的,但它正在引发一个问题。现在当我点击任何选项卡时...比如我点击副作用时,如果RecyclerView的位置在注意事项上,那么它仍然停留在注意事项选项卡上,会有一点抖动。 - Prithvi Bhola
2
这种方式可行@Nilesh。你还需要在TabLayoutListener中设置isUserScrolling = false。然后它就会正常工作。谢谢。 - Prithvi Bhola

4
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()
                }
            }
        })
    }

1

我想你甚至不需要那些标志,只需覆盖RecyclerView的onScrolled方法,在选中选项卡时选择并滚动到位置,前提是该选项卡尚未被选中:

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的自定义视图:

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. 当用户滚动和快速滑动时,选项卡被选中
  2. 当用户点击选项卡时,RecyclerView会滚动。

但是这个解决方案会导致快速滚动时出现卡顿,滚动不流畅,每次只能滚动一个项目。 - NehaK
我明白你的意思,@NehaK。在我的情况下,物品比较大,所以并没有太明显的感觉。也许布局过于复杂,导致了更平滑的转换? - Yurets
不,实际上当我滚动时,所选的选项卡会被选择,然后再次调用scrollToitem,因此recycler view仅停留在该位置,就像view pager的工作方式一样。 - NehaK
但我不想在那个位置暂停,相反滚动应该像原来一样工作,因此Shahab Rauf的解决方案适合我,因为在那里我们正在检查它是否滚动,所以我们在滚动时不会停止。 - NehaK
@NehaK 我明白了,很有道理。 - Yurets

0

我使用了其他答案中的信息,但是代码中有一些遗漏,导致它不完整且工作不好。我的解决方案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 类用于根据位置获取选项卡:

@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()
}

这是屏幕的外观:

TabLayout and RecyclerView screen


0

试试这个:

简单步骤:

  1. 检测RecyclerView滚动状态
  2. 使用findFirstVisibleItemPosition()返回第一个可见视图的适配器位置
  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)
    } 

如果需要,你可以简单地使用 liveData 中的 viewModel。

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)
            }
        }

并处理所选选项卡

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")
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接