【问题标题】:How to sync TabLayout with Recyclerview?如何将 TabLayout 与 Recyclerview 同步?
【发布时间】:2021-06-21 14:52:36
【问题描述】:

我有一个TabLayoutRecyclerview,这样当点击标签时Recyclerview 会滚动到特定位置。 我也想要相反的过程,这样当Recyclerview 滚动到特定位置时,特定选项卡就会突出显示。

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

当“工作原理”出现在TabLayout 下方时,应突出显示“工作原理”选项卡。

【问题讨论】:

    标签: java android android-layout android-recyclerview android-tablayout


    【解决方案1】:

    试试这个

    按照这个步骤进行

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

    【讨论】:

    • 这确实有效,但它正在制造一个问题。现在,当我单击任何选项卡时....假设我正在单击 Side effects,而 Recyclerview 位置位于 Precautions 然后它只停留在 Precautions 选项卡上,有点混蛋。
    • 此方法有效@Nilesh。您还需要在TabLayoutListener 中设置isUserScrolling = false。然后它会正常工作。谢谢。
    【解决方案2】:
    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()
                    }
                }
            })
        }
    

    【讨论】:

      【解决方案3】:

      我认为您甚至不需要这些标志,并且足以覆盖 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,它被设置为 customView 到 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 在用户单击选项卡时滚动。

      【讨论】:

      • 但是这个解决方案导致snap效果,滚动不流畅,逐项滚动
      • 我明白你在说什么@NehaK。在我的情况下,项目更大,所以它并不明显。可能是布局过于复杂导致过渡更流畅?
      • 不,实际上,当我滚动时,尊重的选项卡被选中,然后再次调用 scrollToitem,因此回收器视图仅在该位置停止,就像视图寻呼机的工作方式一样
      • 但我不想在那个位置暂停,而是滚动应该按原样工作,所以 Shahab Rauf 解决方案对我有用,因为我们正在检查它的滚动与否,所以我们不'滚动时不要停止
      • @NehaK 我明白了,有道理。
      【解决方案4】:

      我使用来自另一个答案的信息,但这里有一些代码遗漏,这使得它不完整且工作不正常。我的解决方案 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()
      }
      

      这是屏幕的外观:

      【讨论】:

        【解决方案5】:

        试试这个,

        简单的步骤:

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-04-07
          • 2015-12-22
          • 2021-01-03
          • 2017-04-28
          • 2020-03-27
          相关资源
          最近更新 更多