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的联动关键在于两个监听的设置,同时将上方提及的几个细节注意一下即可;
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现左右联动的方式有很多种,其中一种常用的方法是利用 `RecyclerView` 的滑动监听来实现。 首先,需要在布局文件中定义两个 `RecyclerView`,分别表示左侧和右侧的列表,如下所示: ```xml <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/left_rv" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"/> <androidx.recyclerview.widget.RecyclerView android:id="@+id/right_rv" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"/> </LinearLayout> ``` 接下来,在代码中分别获取这两个 `RecyclerView`,并设置它们的布局管理器和适配器。 ```java RecyclerView leftRv = findViewById(R.id.left_rv); RecyclerView rightRv = findViewById(R.id.right_rv); // 设置布局管理器 leftRv.setLayoutManager(new LinearLayoutManager(this)); rightRv.setLayoutManager(new LinearLayoutManager(this)); // 设置适配器 leftRv.setAdapter(leftAdapter); rightRv.setAdapter(rightAdapter); ``` 接着,在左侧列表的滑动监听中获取当前可见的第一个 item,然后将右侧列表滚动到对应的位置。具体实现如下: ```java leftRv.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); int firstVisibleItemPosition = ((LinearLayoutManager) leftRv.getLayoutManager()).findFirstVisibleItemPosition(); rightRv.scrollToPosition(firstVisibleItemPosition); } }); ``` 这样,当左侧列表滑动时,右侧列表也会跟着滑动,实现了左右联动的效果。需要注意的是,这种实现方式只适用于两个列表的 item 数量相同的情况,如果两个列表的 item 数量不同,则需要进行一些额外的处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值