Android实现带侧滑菜单的二维表格控件

龙旋

共 6502字,需浏览 14分钟

 ·

2022-03-05 21:41

最近在开发一个二维滚动表格的类似于Excel效果的组件并且同时支持侧滑,网上大部分的实现场景都是没有带侧滑菜单的,由于刚好侧滑手势与横向滚动冲突,所以需要同时兼容二者,只能自己动手了,最终部分效果如下:



实现思路


可以看到,想要实现的效果,是无论表格如何滚动,顶部第一行一直处于屏幕的上端,左边第一列一直处于屏幕的左端,就类似于Excel那样,行头和列头的位置是不变的,只有内容区域可以上下左右滚动




基于这样一个基本的规律,一共尝试了以下几种方案:


1、列头+行头+基于GridLayoutManager的内容区


即分为三部分,列头部采用 RecyclerView 或 HorizontalScrollView ,行头部采用 RecyclerView ,除了列头和行头之外的内容区域,直接采用 GridLayoutManager 去实现,这样的好处是不用自己维护多个 RecyclerView 的同步滚动。


由于侧滑是要整体平移腾出空间显示菜单,但是这种方案行头和内容是分开来的,并不是作为一个整体,所以当出现侧滑菜单的时候会有显示的问题。


2、纵向RecyclerView+多个横向RecyclerView


即每一行都是一个 RecyclerView ,然后整个纵向列表又是一个 RecyclerView 的,这种方案的好处是横向和纵向均有复用,但是会有个问题,调用 scrollTo 同步滚动所有子 RecyclerView 的时候,无法惯性滚动,横向滑动体验不太好。


3、纵向RecyclerView+多个LinearLayout+Scroller滑动处理


与双 RecyclerView 的区别在于,每个横向均采用 LinearLayout 实现,但 HorizontalScrollView 本身 scrollTo 是没有惯性的,需要依靠 Scroller 的 fling 来实现惯性的处理。


综上决定采用第三种方案,通过Scroller自行处理滑动逻辑以及滑动边界值的限定,在纵向数据量大的情况也能流畅滚动。


如何实现同步滚动?


这里写了一个 ScrollHelper 类,专门协调所有横向LinearLayout的同步滑动逻辑和监听。


由于是 RecyclerView,所以要实现同步滚动,所以需要记录当前的全局Scroll值,作为所有横向列表滑动距离的唯一参考:

public class ScrollHelper {    private int mScrollX;    public void setScrollX(int x) {        mScrollX = x;    }
public int getCurScrollX() { return mScrollX; }}


另外在 ScrollHelper 中维护一个集合,集合里面维护已经创建的item视图:

private SparseArray itemViewsMap;


然后在adapter每次onBindViewHolder的时候,将item视图以下标position为key,存入集合中,并且滑动至当前的全局mScrollX值。每次onCreateViewHolder的时候,将每个横向布局跟ScrollHelper关联起来,即每个横向LinearLayout都持有这个ScrollHelper实例,以便其中一个LinearLayout滑动的时候,可以通过ScrollHelper中维护的集合通知其它所有LinearLayout。


比如其中某个LinearLayout的滑动触发了其 computeScroll 方法:

@Overridepublic void computeScroll() {    if (mScroller.computeScrollOffset()) {        mScrollHelper.notifyScroll(scrollDistance);        mScrollHelper.setScrollX(scrollDistance);        postInvalidate();    }    super.computeScroll();}


将当前这个LinearLayout滚动的最新值去更新全局scrollX值,并且通知 ScrollHelper 触发其它LinearLayout同步滚动:

public class ScrollHelper {    ...    public void notifyScroll(int x) {        for (int i = 0; i < itemViewsMap.size(); i++) {            itemViewsMap.get(itemViewsMap.keyAt(i)).notifyScroll(x);        }    }    ...}


当然,记得在View被销毁的时候清除集合数据,避免泄露。


如何实现惯性滚动?


主要借助于 Scroller 的 fling 方法,fling可以让View在滑动到某个位置之后以一定的速度惯性滚到一段距离。因此在收到 MotionEvent.ACTION_UP 事件时(即手指抬起时),将惯性滚动前的初始ScrollX值作为fling的起始参数,然后依旧是在 computeScroll 方法里面获取最新滚动到的ScrollX值去同步给其他LinearLayout,实现所有横向列表整体惯性。


如何处理侧滑与横向列表的滑动冲突?


这里的侧滑实现是基于 SwipeRecyclerView 这个库的基础上进行修改,所以这里自定义一个 RecyclerView 继承于 SwipeRecyclerView 重写其 onInterceptTouchEvent 方法,每次收到MotionEvent.ACTION_MOVE 事件的时候,判断当前内容区是否滑到了最左侧界线,分别做如下处理:


如果此时位于最左侧界线,判断当前手势是左滑还是右滑,如果是右滑,那就通过super.onInterceptTouchEvent(e)交由SwipeRecyclerView去处理侧滑逻辑,如果是左滑,则需要return true拦截事件,然后在onTouchEvent中做横向滚动处理。
如果当前不在左侧界线,说明无论左滑还是右滑,横向列表皆有滑动的空间,同样return true拦截事件交由onTouchEvent中做横向滚动处理。

 

如何使用


在项目根目录的build.gradle添加:

allprojects {    repositories {        ...        maven { url 'https://jitpack.io' }    }}


在项目的build.gradle添加如下依赖:

implementation 'com.github.GitHubZJY:XTableView:v1.0.0'


1、在xml中引用XTableView

        android:id="@+id/table_view"        android:layout_width="match_parent"        android:layout_height="wrap_content"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintTop_toTopOf="parent"        app:swipeLayout="@layout/table_swipe_menu_layout"        app:headerHeight="50dp"        app:rowHeight="80dp"        app:cellWidth="130dp">


2、在代码中初始化XTableView

ITableView vTableView = findViewById(R.id.table_view);//设置是否支持长按拖动列表项vTableView.setLongPressDragEnable(true);//设置是否支持侧滑菜单vTableView.setSwipeEnable(true);


3、绑定数据


绑定数据的设计灵感来自于RecyclerView的adapter设计,与数据相关的操作均通过 XTableAdapter 作为中间者来进行:


1、自定义一个adapter继承于 XTableAdapter ,通过实现adapter的方法,自定义表格样式。XTableAdapter 的各个方法作用如下:

onBindHeader 创建列头部视图
onCreateTableItem 创建每一个单元格的视图
onBindTableItem 绑定每一个单元格的视图数据
onCreateRowHeader 创建每一行的头部视图
onBindRowHeader 绑定每一行的头部视图数据

public class CustomTableAdapter extends XTableAdapter>{
public CustomTableAdapter(Context context) { super(context); }
@Override public View onBindHeader(int position, String t) { TextView cellTv = new TextView(getContext()); cellTv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13); cellTv.setGravity(Gravity.CENTER); cellTv.setText(t); return cellTv; }
@Override public View onCreateTableItem(int position) { View view = LayoutInflater.from(getContext()).inflate(R.layout.table_item_cell_layout, null); TextView cellTv = view.findViewById(R.id.cell_tv); cellTv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18); cellTv.setGravity(Gravity.CENTER); return view; }
@Override public void onBindTableItem(int position, View view, TableRowModel rowModel) { final TableRowCellModel cellModel = rowModel.getRowData().get(position); TextView cellTv = view.findViewById(R.id.cell_tv); cellTv.setTextColor(cellModel.isRise() ? getColor(R.color.table_view_rise_txt_color) : getColor(R.color.table_view_fall_txt_color)); cellTv.setText(cellModel.getContent()); cellTv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(getContext(), cellModel.getContent(), Toast.LENGTH_SHORT).show(); } }); }
@Override public View onCreateRowHeader(int position) { return LayoutInflater.from(getContext()).inflate(R.layout.table_item_title_layout, null); }
@Override public void onBindRowHeader(int position, View view, TableRowModel rowModel) { TextView vTitle = view.findViewById(R.id.title_tv); TextView vDetail = view.findViewById(R.id.detail_tv); String title = rowModel.rowHeader.getTitle(); String detail = rowModel.rowHeader.getDetail(); vTitle.setText(TextUtils.isEmpty(title) ? "" : title); vDetail.setText(TextUtils.isEmpty(detail) ? "" : detail); }}


2、通过adapter的 bindData 绑定自定义数据源.

/** * 更新表格数据 * @param header 左上角的表头数据 * @param columnHeader 列头部数据集合 * @param tableData 每一行的数据集合 */public void bindData(String header, List columnHeader, List tableData)


3、将adapter设置给XTableView.

vTableView.setTableAdapter(adapter);


4、数据变更时刷新视图


数据变更也是通过adapter对象来进行:


如果是所有数据替换,可调用 bindData 方法设置新的数据,然后通过 notifyDataSetChanged 进行更新。


如果是单条数据刷新,可调用 notifyItemData(int position, H data) 进行更新,position是对应的下标,data为新的数据。


如果是单条数据插入,可调用 notifyInsertData(int position, H data) 进行更新,position是对应的下标,data为新的数据。


5、设置监听回调

vTableView.setTableListener(new XTableListener() {    @Override    public void clickSwipeMenu(int position) {        //点击菜单        ...    }
@Override public void onItemMove(int fromPos, int toPos) { //拖拽Item ... }});


6、其他属性


本库也提供了LayoutManager的一些配置,例如:setReverseLayout 、 setStackFromEnd 、 scrollToPosition 、 scrollToPositionWithOffset 等,后续会再根据需要进行扩充。


源码地址:

https://github.com/GitHubZJY/XTableView


到这里就结束啦。

浏览 152
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报