【Android】RecyclerView实现表格上下左右、横向纵向双向滑动效果

一、实现效果

Android屏幕显示范围有限,在数据可视化需求中经常要使用这样的表格:横向纵向都能滑动,左侧栏目固定,右边可以整体上下左右滑动。例如股票、证券、课程表、值日表、Excel等等

主要就是分为 标题内容 两部分,其中内容部分一般都是嵌套RecyclerView,使得标题和内容左右横向滑动时是一起滑动的,且不能错位。

效果示例如下:
在这里插入图片描述

二、实现思路

RecyclerView有一个回收和复用机制,ViewHolder可重复回收利用,如下图第三行从屏幕消失时会重新移到第一行复用,所以我们在适配器中使用if语句改变布局,必须要有else处理默认布局,否则复用时还是改变过的布局。
在这里插入图片描述
具体可以自行去分析源码,对于实现复杂效果非常有帮助。
RecyclerView回收复用机制

该效果可以有多种实现方式,根据不同的业务需求去选定:

1、 (标题) HorizontalScrollView、(内容) ListView 【(左)TextView+(右)HorizontalScrollView 】
2、 (标题) HorizontalScrollView、(内容) RecyclerView【(左)TextView+(右)HorizontalScrollView 】 参考文章:安卓使用RecyclerView+HorizontalScrollView 实现Item整体横向滑动
3、(标题) RecyclerView、(内容) RecyclerView【(左)TextView+(右)RecyclerView】

这几种思路大同小异,都是嵌套滑动控件,都能很好地实现效果。第二种需要重写HorizontalScrollView有些麻烦,需要专门去处理HorizontalScrollView 嵌套横向数据过多而导致的性能问题,第三种简洁且性能稍微好点,也是接下来要介绍的。

布局架构:嵌套去实现上下左右滑动
在这里插入图片描述
难点则是:

标题的RecyclerView与下面内容RecyclerView的Item中RecyclerView的同步横向滚动

三、具体实现

三个适配器就能实现:TableTopAdapter和TableRightScrollAdapter左右联动

标题:TableTopAdapter
内容:TableContentAdapter
内容item中的 RecyclerView:TableRightScrollAdapter

下面布局中出现的1dp的view是边框线。

  1. 表格布局xml:
			<LinearLayout
                android:id="@+id/ll_top_root"
                android:layout_width="match_parent"
                android:layout_height="70dp"
                android:background="@android:color/white"
                android:orientation="horizontal"
                android:visibility="visible">

                <androidx.constraintlayout.widget.ConstraintLayout
                    android:layout_width="119dp"
                    android:layout_height="match_parent">

                    <TextView
                        android:id="@+id/tv_left_title_top"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="日期"
                        android:textSize="15sp"
                        app:layout_constraintHorizontal_bias="0.8"
                        app:layout_constraintVertical_bias="0.3"
                        app:layout_constraintBottom_toBottomOf="parent"
                        app:layout_constraintEnd_toEndOf="parent"
                        app:layout_constraintStart_toStartOf="parent"
                        app:layout_constraintTop_toTopOf="parent" />

                    <TextView
                        android:id="@+id/tv_left_title_bottom"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="区域"
                        android:textSize="15sp"
                        app:layout_constraintHorizontal_bias="0.2"
                        app:layout_constraintVertical_bias="0.7"
                        app:layout_constraintBottom_toBottomOf="parent"
                        app:layout_constraintEnd_toEndOf="parent"
                        app:layout_constraintStart_toStartOf="parent"
                        app:layout_constraintTop_toTopOf="parent" />

                    <ImageView
                        android:id="@+id/tv_left_title_line"
                        android:layout_width="150dp"
                        android:layout_height="1dp"
                        android:background="#b8b8b8"
                        app:layout_constraintBottom_toBottomOf="parent"
                        app:layout_constraintStart_toStartOf="parent"
                        app:layout_constraintTop_toTopOf="parent"
                        app:layout_constraintEnd_toEndOf="parent"
                        app:layout_constraintVertical_bias="0.52" />

                </androidx.constraintlayout.widget.ConstraintLayout>
                <View
                    android:layout_width="1dp"
                    android:layout_height="match_parent"
                    android:background="#b8b8b8"/>

                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:orientation="vertical">
                    <androidx.recyclerview.widget.RecyclerView
                        android:id="@+id/recycler_title"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:overScrollMode="never"
                        android:scrollbars="none">
                    </androidx.recyclerview.widget.RecyclerView>
                </RelativeLayout>

            </LinearLayout>
            <View
                android:id="@+id/ll_top_root_line"
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="#b8b8b8"/>
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recycler_content"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1">

在这里插入图片描述
标题固定的单元格中间的线做了一个旋转,达到倾斜分割的显示。

  1. 标题适配器 TableTopAdapter 和 内容item中的右侧适配器 TableRightScrollAdapter:这里比较简单,这就是BaseQuickAdapter的快捷便利,它的使用之前有写过文章:BaseQuickAdapter使用(RecyclerView万能适配器)
class TableTopAdapter: BaseQuickAdapter<String, BaseViewHolder>(R.layout.item_table_scroll)  {
    override fun convert(holder: BaseViewHolder, item: String) {
        holder.setText(R.id.tv_scroll,item)
    }
}

class TableRightScrollAdapter: BaseQuickAdapter<String, BaseViewHolder>(R.layout.item_table_scroll)  {
    override fun convert(holder: BaseViewHolder, item: String) {
        holder.setText(R.id.tv_scroll,item)
    }
}
  1. 单元格布局 item_table_scroll.xml:标题的item和内容item中的右侧item都是使用它,所以叫单元格
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="120dp"
    android:layout_height="70dp"
    android:orientation="horizontal">

    <TextView
        android:gravity="center"
        android:id="@+id/tv_scroll"
        android:layout_width="119dp"
        android:maxLines="3"
        android:ellipsize="end"
        android:layout_height="match_parent"/>
    <View
        android:layout_width="1dp"
        android:layout_height="match_parent"
        android:background="#b8b8b8"/>
</LinearLayout>

内容右侧每一个单元格的宽度和高度都要和标题的单元格一样,否则错位,例如宽度都是119dp的文章加上1dp的边框线

  1. 内容布局 item_table_content.xml:整体上下滑动,其中左侧固定,右侧嵌套左右滑动
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:background="@android:color/white"
    android:layout_height="70dp"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="69dp"
        android:orientation="horizontal">
        <LinearLayout
            android:layout_width="120dp"
            android:orientation="horizontal"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/tv_left_title"
                android:layout_width="119dp"
                android:layout_height="match_parent"
                android:text=""
                android:gravity="center"/>
            <View
                android:layout_width="1dp"
                android:layout_height="match_parent"
                android:background="#b8b8b8"/>
        </LinearLayout>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_item_right"
            android:layout_width="match_parent"
            android:layout_height="69dp"
            android:overScrollMode="never"
            android:scrollbars="none">

        </androidx.recyclerview.widget.RecyclerView>
    </LinearLayout>
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#b8b8b8"/>

</LinearLayout>
  1. 内容适配器 TableContentAdapter :关键联动代码,整体来看联动的就是标题的一个recycleview和内容里面的多个recycleview
//传入标题RecyclerView:recycler_title
class TableContentAdapter(val headRecycler:RecyclerView): BaseQuickAdapter<TableContentBean, BaseViewHolder>(R.layout.item_table_content)  {
    private var firstPos = -1
    private var firstOffset = -1
    
    //保存所有 recyclerView
    private val observerList= hashSetOf<RecyclerView>()
    
    //初始化
    init {
        // 1. 处理头部的一个recycleview联动
        initRecyclerView(headRecycler)
    }
    override fun convert(holder: BaseViewHolder, item: TableContentBean) {
        holder.setText(R.id.tv_left_title,item.leftTitle)
        
        //右边滑动部分
        holder.getView<RecyclerView>(R.id.rv_item_right).layoutManager = LinearLayoutManager(context,LinearLayoutManager.HORIZONTAL,false)
        holder.getView<RecyclerView>(R.id.rv_item_right).setHasFixedSize(true)
        val rightScrollAdapter = TableRightScrollAdapter()
        rightScrollAdapter.setNewInstance(item.rightDates)
        holder.getView<RecyclerView>(R.id.rv_item_right).adapter = rightScrollAdapter
        rightScrollAdapter.notifyDataSetChanged()

        // 2. 处理内容的多个recycleview联动
        initRecyclerView(holder.getView<RecyclerView>(R.id.rv_item_right))
    }

    //所有recycleview联动
    @SuppressLint("ClickableViewAccessibility")
    fun initRecyclerView(recyclerView: RecyclerView) {
        recyclerView.setHasFixedSize(true)
        //获取每一个recycleview的layoutManager
        val layoutManager: LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
        
        //上拉加载更多的时候  保证新加载出来的item 跟已经滑动的item位置保持一致,原来是>0/>0
        if (firstPos >= 0 && firstOffset >= 0) {
        	// 通过移动layoutManager来实现列表滑动,让新加载的item条目保持跟已经滑动的recycleview位置保持一致
            layoutManager.scrollToPositionWithOffset(firstPos + 1, firstOffset)
        }
        
        // 添加所有的 recyclerView
        observerList.add(recyclerView)
        
        //当触摸条目的时候 停止滑动
        recyclerView.setOnTouchListener { view, motionEvent ->
            when (motionEvent.action) {
                MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> for (rv in observerList) {
                    rv.stopScroll()
                }
            }
            false
        }
        
        //当前recycleview的滑动监听。当前滑动,其他所有recycleview都要滑动
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                val linearLayoutManager: LinearLayoutManager =
                    recyclerView.layoutManager as LinearLayoutManager
                //获取第一个item的位置
                val currentFirstPos: Int = linearLayoutManager.findFirstVisibleItemPosition()
                //获取第一个item的View
                val firstVisibleItem= linearLayoutManager.getChildAt(0)
                if (firstVisibleItem != null) {
                    //获取第一个item的偏移量
                    val firstRight: Int = linearLayoutManager.getDecoratedRight(firstVisibleItem)
                    //遍历其它的所有的recycleview
                    for (rv in observerList) {
                        if (recyclerView !== rv) {
                            val mLayoutManager= rv.layoutManager as? LinearLayoutManager
                            mLayoutManager?.let{
                                firstPos = currentFirstPos
                                firstOffset = firstRight
                                //通过当前显示item的位置和偏移量的位置来同步其它item的移动距离
                                mLayoutManager.scrollToPositionWithOffset(firstPos + 1, firstRight)
                            }
                        }
                    }
                }
            }

            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
            }
        })
    }
}

//内容的数据格式
data class TableContentBean (
    val leftTitle: String,
    val rightDates:ArrayList<String>
    ){
}

layoutManager.scrollToPositionWithOffset(firstPos + 1, firstRight)

移动layoutManager来实现条目滑动,通过子条目的横向滑动来联动其它条目同步滑动,从而解决整体滑动的问题,避免嵌套HorizontalScrollView 从而提高性能问题。

  1. Activity中使用:
		private val titleList by lazy{ arrayListOf("星期一","星期二","星期三","星期四","星期五") }
		private val contentList by lazy{ arrayListOf(
		TableContentBean("区域1",arrayListOf("1","2","3","4","5")),
		TableContentBean("区域2",arrayListOf("1","2","3","4","5")),
		) }
		
		//处理标题部分
        recycler_title.layoutManager = LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false)
        val topTabAdapter = TableTopAdapter()
        recycler_title.adapter = topTabAdapter
        topTabAdapter.setNewInstance(topTabs)
        topTabAdapter.notifyDataSetChanged()
        tv_left_title_line.rotation=30f //旋转

        //处理内容部分
        recycler_content.layoutManager = LinearLayoutManager(this)
        recycler_content.setHasFixedSize(true)
        val contentAdapter = TableContentAdapter(recycler_title,currentPos=currentPos)
        recycler_content.adapter = contentAdapter
        contentAdapter.setNewInstance(contentList)
        contentAdapter.notifyDataSetChanged()

总结

五月份也即将结束了,回想去年五月,五一五四难得的假期,抓紧时间在图书馆学习,现在看待学习和当初看待学习的感觉也已截然不同,是非成败转头空。

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
实现九宫格横向左右滑动的功能,可以使用 RecyclerView 和 GridLayoutManager 来实现。以下是具体步骤: 1. 在布局文件中,添加 RecyclerView 控件: ```xml <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollbars="horizontal" /> ``` 2. 在代码中,初始化 RecyclerView 和 GridLayoutManager,并设置相关属性: ```java RecyclerView recyclerView = findViewById(R.id.recycler_view); GridLayoutManager layoutManager = new GridLayoutManager(this, 3); // 设置列数为3 layoutManager.setOrientation(RecyclerView.HORIZONTAL); // 设置水平滚动 recyclerView.setLayoutManager(layoutManager); int spacing = getResources().getDimensionPixelSize(R.dimen.grid_spacing); // 设置item之间的间距 recyclerView.addItemDecoration(new GridSpacingItemDecoration(3, spacing, true)); // 通过自定义ItemDecoration来实现 ``` 其中,GridSpacingItemDecoration 是自定义的一个 RecyclerView.ItemDecoration,用来设置 item 之间的间距,具体实现如下: ```java 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; } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { int position = parent.getChildAdapterPosition(view); // item position int column = position % spanCount; // item column 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) { // top edge outRect.top = spacing; } outRect.bottom = spacing; // item bottom } else { outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing) outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing) if (position >= spanCount) { outRect.top = spacing; // item top } } } } ``` 3. 创建 RecyclerView.Adapter 和 RecyclerView.ViewHolder,并在 onBindViewHolder() 方法中设置 item 的内容: ```java public class GridAdapter extends RecyclerView.Adapter<GridAdapter.GridViewHolder> { private List<Integer> dataList; // 数据源 public GridAdapter(List<Integer> dataList) { this.dataList = dataList; } @NonNull @Override public GridViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_grid, parent, false); return new GridViewHolder(view); } @Override public void onBindViewHolder(@NonNull GridViewHolder holder, int position) { holder.imageView.setImageResource(dataList.get(position)); // 设置图片资源 } @Override public int getItemCount() { return dataList.size(); } static class GridViewHolder extends RecyclerView.ViewHolder { ImageView imageView; GridViewHolder(View itemView) { super(itemView); imageView = itemView.findViewById(R.id.image_view); } } } ``` 4. 最后在 Activity 中设置 RecyclerView 的 adapter: ```java List<Integer> dataList = new ArrayList<>(); // 数据源 dataList.add(R.drawable.pic1); dataList.add(R.drawable.pic2); dataList.add(R.drawable.pic3); dataList.add(R.drawable.pic4); dataList.add(R.drawable.pic5); dataList.add(R.drawable.pic6); dataList.add(R.drawable.pic7); dataList.add(R.drawable.pic8); dataList.add(R.drawable.pic9); GridAdapter adapter = new GridAdapter(dataList); recyclerView.setAdapter(adapter); ``` 这样就可以实现九宫格横向左右滑动效果了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值