【Android】RecyclerView实现表格上下左右、横向纵向双向滑动效果
Android屏幕显示范围有限,在数据可视化需求中经常要使用这样的表格:横向纵向都能滑动,左侧栏目固定,右边可以整体上下左右滑动。例如股票、证券、课程表、值日表、Excel等等主要就是分为 标题 和 内容 两部分,其中内容部分一般都是嵌套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是边框线。
- 表格布局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">
标题固定的单元格中间的线做了一个旋转,达到倾斜分割的显示。
- 标题适配器 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)
}
}
- 单元格布局 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的边框线
- 内容布局 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>
- 内容适配器 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 从而提高性能问题。
- 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)
recycler_content.adapter = contentAdapter
contentAdapter.setNewInstance(contentList)
contentAdapter.notifyDataSetChanged()
总结
五月份也即将结束了,回想去年五月,五一五四难得的假期,抓紧时间在图书馆学习,现在看待学习和当初看待学习的感觉也已截然不同,是非成败转头空。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)