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

一.前言

  • 对于更多功能页面,使用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.height
                itemHeight = view.height
                if (itemTitleHeight == 0) {
    
    
                    val childNumber = (view as ViewGroup).childCount
                    if (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? = null
    private var mManager: LinearLayoutManager? = null

    private var mAllFuncationInfos: MutableList<AllFunctionInfoRes>? = null
    override fun titBarView(view: View): View = mBinding.funcationTitleBar

    override 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.attributes

        if(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].children
            lastItemChildrenEmpty = 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 = mManager
        mBinding.recyclerView.adapter = mAllFuncationRvAdapter
        RecycleViewU.setMaxFlingVelocity(mBinding.recyclerView, 10000)
        initTablayout()
        mAllFuncationRvAdapter!!.notifyDataSetChanged()
    }

    override fun prepareData() {
    
    
        super.prepareData()
        initAdapter()
        setAllFuncationData()
    }

    private fun initTablayout() {
    
    
        mBinding.tabLayout.tabMode = TabLayout.MODE_SCROLLABLE
        for (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 = 1
    private var parentHeight = 0
    private var itemHeight = 0
    private var itemTitleHeight = 0
    private var mSpace: Int = space
    private var mRecyclerView: RecyclerView = recyclerView
    private var mAllFuncationInfos: List<AllFunctionInfoRes> = allFunctionInfoRes
    private var mLayoutResId = layoutResId

    override 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/74735039
            recyclerView.setHasFixedSize(true);

            //可以做一下缓存 避免每次滑动都重新设置
            val itemRecyclerViewAdapter =
                ItemRecyclerViewAdapter(R.layout.item_recycle_inner_content)
            recyclerView.adapter = itemRecyclerViewAdapter
            itemRecyclerViewAdapter.setNewInstance(item.children)

            itemRecyclerViewAdapter.setOnItemClickListener {
    
     adapter, _, position ->
                val childrenBean = adapter.getItem(position) as ChildrenBean
                if (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.height
                itemHeight = view.height
                if (itemTitleHeight == 0) {
    
    
                    val childNumber = (view as ViewGroup).childCount
                    if (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? = null

    interface 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">

    <LinearLayout
        android: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">

        <ImageView
            android: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" />

        <TextView
            android: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.RecyclerView
        android: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的联动关键在于两个监听的设置,同时将上方提及的几个细节注意一下即可;

猜你喜欢

转载自blog.csdn.net/itTalmud/article/details/130330097