一.前言

  • 对于更多功能页面,使用RecycleView与TabLayout联动方式实现是比较常见的,先上效果图(请大佬们忽略gif的水印)

  • 单独使用TabLayout和RecycleView都是比较容易的,这里就不做举例了;gif中的列表实际上是RecycleView嵌套了RecycleView,嵌套的RecycleView设置了间距(不是本文的重点,代码会在下方贴出来),实现item均分;
  • 列表的实现借助了开源库:com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4;
  • 这里个人先讲解实现思路(会配上局部代码,不要在意代码实现),最后再贴出全部的代码;

二.联动效果的实现

  • 联动效果的实现核心在于两个监听的设置。
  • 其一:RecycleView需要设置setOnScrollChangeListener,实现滑动RecyclerView列表的时候,根据最上面一个Item的position来切换TabLayout的tab;
mBinding.recyclerView.setOnScrollChangeListener { _, _, _, _, _ ->mBinding.tabLayout.setScrollPosition(mManager!!.findFirstVisibleItemPosition(),0F,true)}
  • 其二:TabLayout需要设置addOnTabSelectedListener,点击tab的时候,RecyclerView自动滑到该tab对应的item位置;
mBinding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {override fun onTabSelected(tab: TabLayout.Tab) {mManager!!.scrollToPositionWithOffset(tab.position, 0)}override fun onTabUnselected(tab: TabLayout.Tab) {}override fun onTabReselected(tab: TabLayout.Tab) {mManager!!.scrollToPositionWithOffset(tab.position, 0)}})

三.细节补充

  • 当滑动到RecycleView最后一个item的时候,需要让最后一个item能滑动到
    TabLayout的下方位置,这里的处理方式是:

    • 将RecycleView定义两种不同类型的布局
override fun getItemViewType(position: Int): Int {return if (position == mAllFuncationInfos.size) {2} else {mViewTypeItem}}
  • 同时RecycleView的item数量额外+1
 override fun getItemCount(): Int {return mAllFuncationInfos.size + 1}
  • 在onCreateViewHolder方法中针对两种不同的item分别返回不同的布局
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {return if (viewType == mViewTypeItem) {val view = LayoutInflater.from(parent.context).inflate(mLayoutResId, parent, false)view.post {parentHeight = mRecyclerView.heightitemHeight = view.heightif (itemTitleHeight == 0) {val childNumber = (view as ViewGroup).childCountif (childNumber > 0) {itemTitleHeight = view.getChildAt(0).height}}}ItemViewHolder(view)} else {//Footer是最后留白的位置,以便最后一个item能够出发tab的切换//需要考虑一个问题,若二级列表中有数据和没有数据 Footer的高度计算存在区别val view = View(parent.context)if (lastItemChildrenEmpty) {view.layoutParams =ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,parentHeight - itemTitleHeight)} else {view.layoutParams =ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,parentHeight - itemHeight)}ItemViewHolder(view)}}
  • 到此,基本上关键的点都已经完成了,但是呢,还是会有细节。其一:对于TabLayout的addOnTabSelectedListener,如果TabLayout的tab是选中状态,当再次点击的时候,不会执行onTabSelected回调。老规矩,还是上图:

  • 最开始TabLayout选中的tab是索引为0的tab,当列表滑动了,再次点击索引为0的tab,没有出现联动效果,因为这次执行的回调不是onTabSelected,而是onTabReselected,所以对应的处理方案应该很清楚了;

  • 接着讲解其它细节,其二:列表的数据源问题,当传递给嵌套的RecycleView的列表数据为空时,且是最后一个item为空,那么底部留白的高度需要重新计算,在前面onCreateViewHolder方法代码已经贴出相关的代码了。

四.代码环节

  • 相关的全部代码
//界面
@Route(path = RouterPathFragment.HomeFour.PAGER_HOME_FOUR)
class ModuleFragment04 :BaseSimpleFragment<ModuleFragment04FragmentHome04Binding>(ModuleFragment04FragmentHome04Binding::inflate) {private val mSpace = DensityU.dip2px(6F)private var mAllFuncationRvAdapter: AllFuncationRvAdapter? = nullprivate var mManager: LinearLayoutManager? = nullprivate var mAllFuncationInfos: MutableList<AllFunctionInfoRes>? = nulloverride fun titBarView(view: View): View = mBinding.funcationTitleBaroverride fun perpareWork() {super.perpareWork()mBinding.funcationTitleBar.leftView.isVisible = false}override fun prepareListener() {super.prepareListener()//滑动RecyclerView list的时候,根据最上面一个Item的position来切换tab
//        mBinding.recyclerView.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->mBinding.recyclerView.setOnScrollChangeListener { _, _, _, _, _ ->mBinding.tabLayout.setScrollPosition(mManager!!.findFirstVisibleItemPosition(),0F,true)}mBinding.tabLayout.setSelectedTabIndicatorColor(ContextCompat.getColor(requireContext(),R.color.color_000000))mBinding.tabLayout.setTabTextColors(ContextCompat.getColor(requireContext(), R.color.color_ff585858),ContextCompat.getColor(requireContext(), R.color.color_000000))mBinding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {override fun onTabSelected(tab: TabLayout.Tab) {//点击tab的时候,RecyclerView自动滑到该tab对应的item位置//当tab是选中状态,再次点击是不会回调该方法,将下方代码在onTabReselected回调中添加即可解决问题mManager!!.scrollToPositionWithOffset(tab.position, 0)}override fun onTabUnselected(tab: TabLayout.Tab) {}override fun onTabReselected(tab: TabLayout.Tab) {mManager!!.scrollToPositionWithOffset(tab.position, 0)}})mAllFuncationRvAdapter!!.setOpenFunctionActivityInterface(object :AllFuncationRvAdapter.OpenFunctionActivityInterface{override fun openFunctionActivity(childrenBean: AllFunctionInfoRes.ChildrenBean) {openActivityByFunction(childrenBean)}})}private fun openActivityByFunction(childrenBean: AllFunctionInfoRes.ChildrenBean) {val attributesBean: AttributesBean? = childrenBean.attributesif(attributesBean != null){if(attributesBean.appFunctionName == "CardLayout"){openActivityByARouter(RouterPathActivity.SimpleRv.PAGER_SIMPLE_RV);}}}private fun initAdapter() {mAllFuncationInfos = mutableListOf()val jsonListInfos = JsonU.json2List(jsonFileName = "treeListInfo.json",clazz = AllFunctionInfoRes::class.java)if (!jsonListInfos.isNullOrEmpty()) {mAllFuncationInfos!!.addAll(jsonListInfos)}if (!mAllFuncationInfos.isNullOrEmpty()) {val itemChildren =mAllFuncationInfos!![mAllFuncationInfos!!.size - 1].childrenlastItemChildrenEmpty = itemChildren!!.isEmpty()}}var lastItemChildrenEmpty = false@SuppressLint("NotifyDataSetChanged")private fun setAllFuncationData() {mAllFuncationRvAdapter = AllFuncationRvAdapter(mAllFuncationInfos!!, lastItemChildrenEmpty,mBinding.recyclerView, mSpace, R.layout.item_all_funcation)mManager = LinearLayoutManager(context)mBinding.recyclerView.layoutManager = mManagermBinding.recyclerView.adapter = mAllFuncationRvAdapterRecycleViewU.setMaxFlingVelocity(mBinding.recyclerView, 10000)initTablayout()mAllFuncationRvAdapter!!.notifyDataSetChanged()}override fun prepareData() {super.prepareData()initAdapter()setAllFuncationData()}private fun initTablayout() {mBinding.tabLayout.tabMode = TabLayout.MODE_SCROLLABLEfor (i in mAllFuncationInfos!!.indices) {val allFunctionInfoRes = mAllFuncationInfos!![i]mBinding.tabLayout.addTab(mBinding.tabLayout.newTab().setText(allFunctionInfoRes.name).setTag(i))}}}//适配器
class AllFuncationRvAdapter(allFunctionInfoRes: MutableList<AllFunctionInfoRes>,private var lastItemChildrenEmpty: Boolean,recyclerView: RecyclerView,space: Int,layoutResId: Int
) : BaseQuickAdapter<AllFunctionInfoRes, BaseViewHolder>(layoutResId, data = allFunctionInfoRes) {private val mViewTypeItem = 1private var parentHeight = 0private var itemHeight = 0private var itemTitleHeight = 0private var mSpace: Int = spaceprivate var mRecyclerView: RecyclerView = recyclerViewprivate var mAllFuncationInfos: List<AllFunctionInfoRes> = allFunctionInfoResprivate var mLayoutResId = layoutResIdoverride fun convert(holder: BaseViewHolder, item: AllFunctionInfoRes) {//负责将每一个将每一个子项holder绑定数据if (holder.itemViewType == mViewTypeItem) {holder.setText(R.id.item_title_tv, item.name)holder.setImageResource(R.id.item_titie_iv, R.drawable.icon_three)val recyclerView = holder.getView<RecyclerView>(R.id.item_recycler_view)recyclerView.setHasFixedSize(true)recyclerView.layoutManager =GridLayoutManager(ContextU.context(), 4,GridLayoutManager.VERTICAL, false)if (recyclerView.itemDecorationCount == 0) {    //只能设置一次recyclerView.addItemDecoration(GridSpacingItemDecoration(4,mSpace,true))}//            当我们确定Item的改变不会影响RecyclerView的宽高的时候可以设置setHasFixedSize(true)
//            https://blog.csdn.net/wsdaijianjun/article/details/74735039recyclerView.setHasFixedSize(true);//可以做一下缓存 避免每次滑动都重新设置val itemRecyclerViewAdapter =ItemRecyclerViewAdapter(R.layout.item_recycle_inner_content)recyclerView.adapter = itemRecyclerViewAdapteritemRecyclerViewAdapter.setNewInstance(item.children)itemRecyclerViewAdapter.setOnItemClickListener { adapter, _, position ->val childrenBean = adapter.getItem(position) as ChildrenBeanif (mOpenFunctionActivityInterface != null) {mOpenFunctionActivityInterface!!.openFunctionActivity(childrenBean)}}}}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {return if (viewType == mViewTypeItem) {val view = LayoutInflater.from(parent.context).inflate(mLayoutResId, parent, false)view.post {parentHeight = mRecyclerView.heightitemHeight = view.heightif (itemTitleHeight == 0) {val childNumber = (view as ViewGroup).childCountif (childNumber > 0) {itemTitleHeight = view.getChildAt(0).height}}}ItemViewHolder(view)} else {//Footer是最后留白的位置,以便最后一个item能够出发tab的切换//需要考虑一个问题,若二级列表中有数据和没有数据 Footer的高度计算存在区别val view = View(parent.context)if (lastItemChildrenEmpty) {view.layoutParams =ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,parentHeight - itemTitleHeight)} else {view.layoutParams =ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,parentHeight - itemHeight)}ItemViewHolder(view)}}override fun getItemCount(): Int {return mAllFuncationInfos.size + 1}//若使用Java语言开发,则不需要做该处理override fun getItem(position: Int): AllFunctionInfoRes {//需要重写一下该方法做特殊处理if (position == mAllFuncationInfos.size) {       //做拦截处理 避免 super.getItem(position)执行时出现索引越界return AllFunctionInfoRes()                  //返回一个空的AllFunctionInfoRes即可}return super.getItem(position)}override fun getItemViewType(position: Int): Int {return if (position == mAllFuncationInfos.size) {2} else {mViewTypeItem}}internal inner class ItemViewHolder(itemView: View) : BaseViewHolder(itemView)//使用接口回调private var mOpenFunctionActivityInterface: OpenFunctionActivityInterface? = nullinterface OpenFunctionActivityInterface {fun openFunctionActivity(childrenBean: ChildrenBean)}fun setOpenFunctionActivityInterface(openFunctionActivityInterface: OpenFunctionActivityInterface) {mOpenFunctionActivityInterface = openFunctionActivityInterface}
}//适配的布局文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="wrap_content"tools:ignore="ResourceName"><LinearLayoutandroid:id="@+id/item_title"android:layout_width="match_parent"android:layout_height="@dimen/dp_30"android:orientation="horizontal"android:gravity="center_vertical"android:layout_marginLeft="@dimen/dp_7"android:layout_marginRight="@dimen/dp_7"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintTop_toTopOf="parent"><ImageViewandroid:id="@+id/item_titie_iv"android:layout_width="@dimen/dp_10"android:layout_height="@dimen/dp_10"android:src="@drawable/icon_three"android:layout_marginLeft="@dimen/dp_8" /><TextViewandroid:id="@+id/item_title_tv"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginLeft="@dimen/dp_4"android:textSize="@dimen/sp_15" /></LinearLayout><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/item_recycler_view"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginTop="@dimen/dp_7"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/item_title"/></androidx.constraintlayout.widget.ConstraintLayout>//Rv间距设置工具类
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {private int     spanCount;private int     spacing;private boolean includeEdge;public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {this.spanCount = spanCount;this.spacing = spacing;this.includeEdge = includeEdge;}public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, RecyclerView parent, @NonNull RecyclerView.State state) {int position = parent.getChildAdapterPosition(view); // 获取view 在adapter中的位置int column = position % spanCount; // view 所在的列if (includeEdge) {outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)if (position < spanCount) { // 第一行outRect.top = spacing;}outRect.bottom = spacing;} else {//等间距需满足两个条件://1.各个模块的大小相等,即 各列的left+right 值相等;//2.各列的间距相等,即 前列的right + 后列的left = 列间距;//公式是需要推演的[演示了当列数为2或者3的时候,验证了公式是成立的]: 资料---https://blog.csdn.net/JM_beizi/article/details/105364227//注:这里用的所在列数为从0开始outRect.left = column * spacing / spanCount; //某列的left = 所在的列数 * (列间距 * (1 / 列数))outRect.right = spacing - (column + 1) * spacing / spanCount; //某列的right = 列间距 - 后列的left = 列间距 -(所在的列数+1) * (列间距 * (1 / 列数))if (position >= spanCount) {    //说明不是在第一行outRect.top = spacing;}}}
}

五.总结

  • TabLayout和RecycleView的联动关键在于两个监听的设置,同时将上方提及的几个细节注意一下即可;

RecycleView与TabLayout联动展示更多功能列表页面的实现相关推荐

  1. 实战SSM_O2O商铺_42【前端展示】店铺列表页面View层的实现

    文章目录 概述 代码结构 shoplist.html shoplist.js shoplist.css common.js添加解析日期的公共方法 FrontEndController添加路由 联调测试 ...

  2. 实战SSM_O2O商铺_41【前端展示】店铺列表页面Dao+Service+Controller层的实现

    文章目录 概述 Dao层 接口 映射文件 单元测试 Service层 接口方法 单元测试 Controller层 增加 ShopListController 单元测试 Github地址 概述 在完成了 ...

  3. vue 滑动加载列表 php,通过原生vue添加滚动加载更多功能

    这篇文章主要介绍了通过原生vue添加滚动加载更多功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 vue中添加滚动加载更多,因为是单页面所以需要在 ...

  4. 微信朋友圈,QQ空间,微博等列表展示的功能实现

    内容摘要 该控件能够应用于内容资讯展示的功能模块中,如:腾讯和新浪微博的微博列表,微信朋友圈及其它社交类应用的好友动态展示列表等:实现了类似腾讯微博的微博列表展示功能,包含微博文本内容,表情,图片,话 ...

  5. 《AngularJS深度剖析与最佳实践》一1.5 实现更多功能:主题

    本节书摘来自华章出版社<AngularJS深度剖析与最佳实践>一书中的第1章,第1.5节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区"华章计算机"公众号查看 ...

  6. VR全景展示是什么,VR全景展示的功能有哪些?

    最近,一种新奇的图像展示方式走进了人们的视野.微博,博客,抖音,朋友圈等平台出现很多能720°无死角浏览的展示作品,吸引了用户大量的点击.这就是VR全景展示,一种新兴富媒体技术,与以往传统的文字图文视 ...

  7. CRM项目之stark组件url的视图函数和列表页面基本展示2

    页面上展示数据表的表头 我们注册了UserInfo表之后,在视图函数change_list_view中执行data_list = self.model_class.objects.all()就可以拿到 ...

  8. js加css实现div展示更多隐藏内容

    说明 在设计博客首页文章分类等栏目时,有时候列表内容太多往往不是一次性展示出来.此时需要添加更多功能,当点击更多标签时再展示剩余隐藏的项目. 效果 代码 <!DOCTYPE html> & ...

  9. java多功能钟_Java 11将包含更多功能

    java多功能钟 Java 11即将发布的功能是什么?它与Java 9和10有何不同? Java 10可能是新手,但现在该谈论Java 11了.Oracle迈向更快的发布周期意味着更多的特性和功能以比 ...

最新文章

  1. VoWi-Fi能给LTE时代的语音通信体验带来什么?
  2. JS factory
  3. 前端学习(2968):实现路由跳转的两种方式
  4. JAVA手写ArrayList以及LinkedList
  5. 链式调用方法的实现原理和方法
  6. Netty简单样例分析[转]
  7. python弱类型好处_JavaScript弱类型语言的优缺点有哪些
  8. 语言做的表白魔方_程序员表白教程,这些代码用过的都说浪漫
  9. shell编写一键安装mysql.sh
  10. 嵌入式操作系统和普通操作系统的区别_嵌入式ARM和单片机的区别何在
  11. OpenProj: The OpenSource Solution for Managing Your Projects
  12. c语言取反运算详细步骤,C语言之位运算详解
  13. 疯狂模渲大师体验版安装教程|效果图设计师怎么安装并注册3dmax疯狂模渲大师体验版?
  14. python-opencv视频转图片+图片转视频(步骤详解)(亲测有效)
  15. SHELL DATE 命令详解
  16. java中创建dvd_JAVA简单模拟DVD功能
  17. 博图WINCC报表(SQL数据库的建立,TIA_wincc在数据库中保存和查询数据,调用Excel模板把数据保存到指定的位置)
  18. Sampler 在数据下沉模式超时; 不同Sampler策略,在非数据下沉模式下,模型训练失败 报错Segmentation fault(core dumped)
  19. html背景音乐自动播放embed,怎样在网页中插入背景音乐(自动播放代码).doc
  20. 未能加载文件或程序集“Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=

热门文章

  1. 90网论坛php基础
  2. 必备干货外贸技巧,你拥有了吗?
  3. 如何提高采购效率?采购询价的标准流程
  4. 赛门铁克VCS(Veritas Cluster Server)双机日常管理
  5. Spring Data Elasticsearch聚合搜索实战
  6. 为什么总是在电路里摆两个0.1uF和0.01uF的电容?
  7. 中颖电动车控制器量产方案 中颖电动车控制器量产方案,霍尔FOC算法,霍尔角度自学习,防飞车,堵转保护等功能
  8. AI人工智能发展的经典算法
  9. 2021高考成绩查询倒计时,距离2021高考时间还有多少天 2021高考倒计时查询
  10. 静态站点生成(SSG)——Gridsome