前言

  最近项目中接触的表格设计需求有点多,表格很简单,但是这个表格的滑动需求是:左右滑动时行标题那一列不能动,列标题是可以左右滑动的;同理,上下滑动时列标题那一列不能动,行标题是可以上下滑动的。自己也研究了网上一些博客和开源项目,总体上来说大同小异,最核心的东西就是如何控制滑动,现在先来看看效果:

android listview 横向滚动 android横向滚动table_布局文件


分析

  我们先来分析左右方向的滑动,左边的row头部和右边cell(这里指的是行列组成的右下角部分)内容是分离的,左边的row头先不管,cell内容(不包括上方的列标题)可以左右滑动我们可以选择HorizontalScrollView作为cell的容器,里面的列表展示可以用RecyclerView,至于RecyclerView的item是什么我们先不管。这时左边的row头部也是列表,我们也可以用垂直方向的RecyclerView,左上角“表格”不管上下左右滑动都是不动的,所以它不可能是滑动控件的一部分。这里我将它当作是列标题的一部分,因为列表题是可以左右滑动的,我们也用HorizontalScrollView作为列标题的容器,至于列标题的具体内容先不管,但是这里列标题还应包括固定不变的“表格”View,可以用线性布局把固定不变的左上角部分和列标题的容器放在一起。这时侯我们就可以把列表题的HorizontalScrollView和cell内容的HorizontalScrollView关联起来,互相监听滑动就可以简单地实现cell和列标题的左右滑动,我们只监听左右,当cell上下滑动时就不会影响列标题了。

  同理表格的上下滑动就变得比较简单了,我们可以用NestedScrollView作为行标题和cell上下滑动的父容器,其实分析了这么多,整体的布局都已经出来了,大概设计:


android listview 横向滚动 android横向滚动table_xml_02

至于HorizontalScrollView1和HorizontalScrollView2的item如何布局呢?考虑列标题的数量,我们采用RecyclerView作为其子View,这样我们就可以控制item的数量来控制列标题了,增加列标题或控制点击事件都比较方便(数量少的话你也可以罗列所有列标题内容);同理,cell内容子View是竖直方向的RecyclerView,但是其item这里我考虑的是嵌套横向的RecyclerView,方便控制其横向(和列标题是对应关系)的内容数量。


代码设计

1.布局文件

主布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/ll_content_table"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="@dimen/corner_view_height"
        android:orientation="horizontal">

        <!--表格内容左右滑动时的阴影竖直线-->
        <com.lihang.ShadowLayout
            android:id="@+id/shadow_layout_column"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            app:hl_shadowColor="@android:color/transparent"
            app:hl_shadowHiddenBottom="true"
            app:hl_shadowHiddenLeft="true"
            app:hl_shadowHiddenRight="true"
            app:hl_shadowHiddenTop="true"
            app:hl_shadowLimit="2dp">

            <TextView
                android:id="@+id/tv_table_title"
                android:layout_width="@dimen/corner_view_width"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="表格"
                android:textColor="#000000"
                android:textSize="14sp" />
        </com.lihang.ShadowLayout>

        <com.littlejerk.multiSample.table.blog.SyncHorizontalScrollView
            android:id="@+id/scroll_view_column_container"
            android:layout_width="match_parent"
            android:background="#EAEAEA"
            android:layout_height="wrap_content">

            <!--RecyclerView必须嵌套RelativeLayout,不然会显示不全-->
            <RelativeLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content">

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/rv_column_header"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:overScrollMode="never" />
            </RelativeLayout>

        </com.littlejerk.multiSample.table.blog.SyncHorizontalScrollView>
    </LinearLayout>
<!--    <View-->
<!--        android:layout_width="match_parent"-->
<!--        android:background="#EAEAEA"-->
<!--        android:layout_height="1dp"/>-->

    <!--下拉刷新和上拉加载-->
    <com.scwang.smart.refresh.layout.SmartRefreshLayout
        android:id="@+id/smartRefresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:srlDisableContentWhenLoading="true"
        app:srlEnableAutoLoadMore="false"
        app:srlEnableRefresh="false">

        <!--竖直滚动-->
        <androidx.core.widget.NestedScrollView
            android:id="@+id/scroll_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="none">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">

                <!--表格内容左右滑动时的阴影竖直线-->
                <com.lihang.ShadowLayout
                    android:id="@+id/shadow_layout_row"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    app:hl_shadowColor="@android:color/transparent"
                    app:hl_shadowHiddenBottom="true"
                    app:hl_shadowHiddenLeft="true"
                    app:hl_shadowHiddenRight="true"
                    app:hl_shadowHiddenTop="true"
                    app:hl_shadowLimit="2dp">
                    <!-- 左侧header的父容器 -->
                    <androidx.recyclerview.widget.RecyclerView
                        android:background="#EAEAEA"
                        android:id="@+id/rv_row_header"
                        android:layout_width="@dimen/corner_view_width"
                        android:layout_height="wrap_content" />
                </com.lihang.ShadowLayout>

                <!-- 右侧内容的父容器 实现水平滚动 -->
                <com.littlejerk.multiSample.table.blog.SyncHorizontalScrollView

                    android:id="@+id/scroll_view_cell_container"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:scrollbars="none">

                    <!-- 必须设置焦点,不然显示隐藏阴影会造成竖直滚动 -->
                    <RelativeLayout
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:descendantFocusability="blocksDescendants">


                        <androidx.recyclerview.widget.RecyclerView
                            android:id="@+id/rv_cell_parent"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:overScrollMode="never" />

                    </RelativeLayout>

                </com.littlejerk.multiSample.table.blog.SyncHorizontalScrollView>
            </LinearLayout>

        </androidx.core.widget.NestedScrollView>
    </com.scwang.smart.refresh.layout.SmartRefreshLayout>

</LinearLayout>

这里也可以加上下拉刷新和上拉加载控件,具体看需求咯,至于ShadowLayout是为了控制左右滑动时行标题和cell内容的阴影分割,迫于设计和产品姐姐的yin…嗯大家都懂,真的苦啊,天天改需求,天天改bug,天天加班,天天…!

其它item的布局文件:

  1. 列标题布局文件item_table_noreuse_column:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/column_width"
    android:layout_height="@dimen/corner_view_height"
    android:layout_gravity="center">

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="测试"
        android:padding="3dp"
        android:textSize="14sp" />
</LinearLayout>
  1. 行标题布局文件item_table_noreuse_row:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/corner_view_width"
    android:layout_height="@dimen/row_height"
    android:layout_gravity="center">

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:padding="3dp"
        android:text="测试"
        android:textSize="14sp" />
</LinearLayout>
  1. cell的父布局文件item_table_noreuse_cell_parent:
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView 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:id="@+id/rv_cell_child"
    android:layout_width="match_parent"
    android:layout_height="@dimen/row_height"
    android:orientation="horizontal"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    tools:listitem="@layout/item_table_blog_cell_child" />
  1. cell的父布局RecyclerView的item布局文件item_table_noreuse_cell_child:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/column_width"
    android:layout_height="@dimen/row_height"
    android:layout_gravity="center">

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="测试"
        android:padding="3dp"
        android:textSize="14sp" />
</LinearLayout>

dimens文件:

<dimen name="corner_view_width">50dp</dimen>
    <dimen name="corner_view_height">50dp</dimen>

    <dimen name="column_width">100dp</dimen>
    <dimen name="row_height">40dp</dimen>

2.同步滑动的SyncHorizontalScrollView文件

public class SyncHorizontalScrollView extends HorizontalScrollView {

    private View mView;
    private ShadowLayout shadowLayout;

    private boolean isShowShadow;

    public SyncHorizontalScrollView(Context context) {
        this(context, null);
    }

    public SyncHorizontalScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SyncHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //取消滑动到最前和最后是出现的蓝色颜色阴影块
        setOverScrollMode(View.OVER_SCROLL_NEVER);
    }

    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);

        //隐藏显示滑动边界阴影
        if (shadowLayout != null) {
            if (l == 0) {
                if (isShowShadow) {
                    shadowLayout.setShadowHiddenRight(true);
                    isShowShadow = false;
                }
            } else {
                if (!isShowShadow) {
                    shadowLayout.setShadowHiddenRight(false);
                    //绘制边界阴影颜色
                    shadowLayout.setShadowColor(Color.parseColor("#1a000000"));
                    shadowLayout.setShadowOffsetY(2);
                    isShowShadow = true;
                }

            }
        }

        //设置控件滚动监听,得到滚动的距离,然后让传进来的view也设置相同的滚动具体
        if (mView != null) {
            mView.scrollTo(l, t);
        }
    }

    /**
     * 设置联动阴影
     *
     * @param shadowLayout
     */
    public void setShadowLayout(ShadowLayout shadowLayout) {
        this.shadowLayout = shadowLayout;
    }

    /**
     * 设置跟它联动的view
     *
     * @param view
     */
    public void setScrollView(View view) {
        mView = view;
    }
}

这个View只要是让左右滑动能够联动起来,代码很简单。

3.Activity逻辑

public class VHSlideTableActivity extends AppCompatActivity {

    //如果有分页
    private SmartRefreshLayout mRefreshLayout;
    private ShadowLayout mShadowLayoutColumn;
    private ShadowLayout mShadowLayoutRow;

    private SyncHorizontalScrollView mSyncHsvColumnContainer;
    private SyncHorizontalScrollView mSyncHsvCellContainer;

    private RecyclerView mRvColumnHeader;
    private RecyclerView mRvRowHeader;
    private RecyclerView mRvCellParent;

    private CommonAdapter mColumnAdapter, mRowAdapter;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_vhslide_table_noreuse);

        mRefreshLayout = findViewById(R.id.smartRefresh);
        mShadowLayoutRow = findViewById(R.id.shadow_layout_row);
        mShadowLayoutColumn = findViewById(R.id.shadow_layout_column);
        mSyncHsvCellContainer = findViewById(R.id.scroll_view_cell_container);
        mSyncHsvColumnContainer = findViewById(R.id.scroll_view_column_container);

        mRvColumnHeader = findViewById(R.id.rv_column_header);
        mRvRowHeader = findViewById(R.id.rv_row_header);
        mRvCellParent = findViewById(R.id.rv_cell_parent);


        List<String> columnList = getColumnHeaderList();
        List<String> rowList = getRowHeaderList();
        List<List<String>> cellList = getCellList();

        mSyncHsvColumnContainer.setScrollView(mSyncHsvCellContainer);
//        mSyncHsvColumnContainer.setShadowLayout(mShadowLayoutColumn);
        mRvColumnHeader.setLayoutManager(new LinearLayoutManager(this, RecyclerView.HORIZONTAL, false));
        mColumnAdapter = new CommonAdapter(R.layout.item_table_noreuse_column, columnList);
        mColumnAdapter.setOnItemChildClickListener((adapter, view, position) -> {
            String name = mColumnAdapter.getItem(position);
            Toast.makeText(this, name, Toast.LENGTH_SHORT).show();
        });
        mRvColumnHeader.setAdapter(mColumnAdapter);
        mRvColumnHeader.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.HORIZONTAL));

        mRvCellParent.setLayoutManager(new LinearLayoutManager(this));
        mRvCellParent.setAdapter(new CellParentAdapter(R.layout.item_table_noreuse_cell_parent, cellList));
        mRvCellParent.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));

        mSyncHsvCellContainer.setScrollView(mSyncHsvColumnContainer);
//        mSyncHsvCellContainer.setShadowLayout(mShadowLayoutRow);
        mRvRowHeader.setLayoutManager(new LinearLayoutManager(this));
        mRowAdapter = new CommonAdapter(R.layout.item_table_noreuse_row, rowList);
        mRowAdapter.setOnItemChildClickListener((adapter, view, position) -> {
            String name = mRowAdapter.getItem(position);
            Toast.makeText(this, name, Toast.LENGTH_SHORT).show();
        });
        mRvRowHeader.setAdapter(mRowAdapter);
        mRvRowHeader.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));


    }

    public class CellParentAdapter extends BaseQuickAdapter<List<String>, BaseViewHolder> {

        public CellParentAdapter(int layoutResId, @Nullable List<List<String>> data) {
            super(layoutResId, data);
        }

        @Override
        protected void convert(@NotNull BaseViewHolder holder, List<String> data) {
            RecyclerView recyclerView = holder.getView(R.id.rv_cell_child);
            recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), RecyclerView.HORIZONTAL, false));
            CommonAdapter commonAdapter = new CommonAdapter(R.layout.item_table_noreuse_cell_child, data);
            if (recyclerView.getItemDecorationCount() == 0) {
                recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.HORIZONTAL));
            }
            commonAdapter.setOnItemChildClickListener((adapter, view, position) -> {
                String name = commonAdapter.getItem(position);
                Toast.makeText(getContext(), name, Toast.LENGTH_SHORT).show();
            });
            recyclerView.setAdapter(commonAdapter);

        }
    }

    public class CommonAdapter extends BaseQuickAdapter<String, BaseViewHolder> {

        public CommonAdapter(int layoutResId, @Nullable List<String> data) {
            super(layoutResId, data);
            addChildClickViewIds(R.id.tv_name);
        }

        @Override
        protected void convert(@NotNull BaseViewHolder holder, String s) {
            holder.setText(R.id.tv_name, s);
        }
    }
}

4.数据

public List<String> getColumnHeaderList() {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 15; i++) {
            list.add("column " + i);
        }
        return list;
    }

    public List<String> getRowHeaderList() {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            list.add("row " + i);
        }
        return list;
    }

    public List<List<String>> getCellList() {
        List<List<String>> cellList = new ArrayList<>();
        for (int i = 0 ;i<100;i++){
            List<String> list = new ArrayList<>();
            for (int j = 0; j < 15; j++) {
                list.add("row" + i + " column"+j);
            }
            cellList.add(list);
        }
        return cellList;
    }

这里主要是数据和列表的展示,其中使用DividerItemDecoration左右内容表格的分割线,你也可以在item布局中设计你所需要的表格分割线,具体看产品姐姐的需求吧。这里用到了几个依赖:

//强大的RecyclerView适配器
implementation ‘com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4’
//刷新控件,不用可去掉
implementation ‘com.scwang.smart:refresh-layout-horizontal:2.0.0’
implementation ‘com.scwang.smart:refresh-layout-kernel:2.0.1’
implementation ‘com.scwang.smart:refresh-header-classics:2.0.1’ //经典刷新头
implementation ‘com.scwang.smart:refresh-header-material:2.0.1’ //谷歌刷新头
//阴影库
implementation ‘com.github.lihangleo2:ShadowLayout:3.2.0’