Android自定义控件实例(2)——AndroidTableView,支持行列合并

一、前言
由于项目中许多数据涉及到表格展示,而且表格控件最好能够支持跨行、跨列合并。鉴于能不重复造轮子就不造的思想,去github上搜索了一番,SortableTableView和AsymmetricGridView都还算是点击量比较高的两个开源项目。但前者不支持跨行列合并,后者只支持2行2列的合并(Currently only has good support for items with rowSpan = 2 and columnSpan = 2.)。如果觉着跨行改成单行、并用重复值填充能够接受的话,SortableTableView还是挺不错的。
由于没有找到支持表格的跨行列合并的开源项目,只能自己动手自定义一个支持跨行列合并的表格控件。
二、思路
一开始想基于GridView或者TableLayout实现跨行列合并,通过实践后发现,GridView本身并不支持跨行合并,而TableLayout是通过添加TableRow来控制表格的行,虽然可通过嵌套TableLayout的方式实现跨行合并,但针对跨行单元格的行有交叉的情况,无法支持。如下图所示:

最后想到,GridLayout(网格布局)本身可以将整个容器划分成n行*m列个网格,每个网格可放置一个组件,并且支持组件跨行、跨列。因此完全可以借助于GridLayout实现作者的需求。
三、基于GridLayout实现自定义表格控件
既然GridLayout支持跨行、跨列合并,那就简单了。直接通过代码往GridLayout里添加TextView或其他组件,并设置其行列号和跨行列个数不就OK了?实践证明我想的太简单了,还是对GridLayout布局使用不熟悉导致的。GridLayout本身不支持滚动条,超出其显示范围的组件会被隐藏掉。如下图所示:


因此,为了使表格支持滚动条,需要将GridLayout布局放到ScrollView中,并将GridLayout的高度设置为wrap_content。此外在ScrollView的上部加一个LinearLayout,用以展示表格的表头。本文自定义表格控件布局设计示意图如下:


四、编码过程中遇到的难点
1、表头与表体数据对齐:
为了使得表头和表数据各列分割线对齐,需要借助于LinearLayout的weight属性。然后用表头的宽度乘以表格数据中各列的跨度再除以总列数即为每个数据格的宽度。即:TableViewWidth*ColumnSpan/ColumnCount。数据格的高度可以用WRAP_CONTENT。
注:一开始试了GridLayout.Sepc的weight参数,但展示有问题,表头的列和数据的列不对齐,具体原因没查出来。截图如下:



2、如何得到表格主体的宽度
为了绘制表格主体(GridLayout),需要先得到表格的宽度,得到这个宽度后,才能设置每个数据格的宽度。表格的宽度即表头(LinearLayout)的宽度,在调用OnMeasure之前,无法知道表头的宽度。因此,本文将表格主体的绘制放到表头的ViewTreeObserver.addOnGlobalLayoutListener()回调方法中。示意代码如下:
ViewTreeObserver viewTreeObserver = tableHeaderView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        tableHeaderView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        tableDataAdapter.setTableDataViewWidth(tableHeaderView.getWidth());
        setupTableDataView();
        forceRefresh();
    }
});

五、代码结构图如下

六、自定义合并单元格示例
MainActivity布局、代码文件和截图如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.wjk.tableview.TableView
        android:id="@+id/tableview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="0dp"></com.wjk.tableview.TableView>

</LinearLayout>

package com.wjk.androidtableview;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Pair;

import com.wjk.tableview.TableView;
import com.wjk.tableview.common.TableCellData;
import com.wjk.tableview.common.TableHeaderColumnModel;
import com.wjk.tableview.toolkits.SimpleTableDataAdapter;
import com.wjk.tableview.toolkits.SimpleTableHeaderAdapter;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class MainActivity extends AppCompatActivity {

    private TableView tableView;
    private Map<Integer, Pair<String,Integer>> columns;

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

        tableView = (TableView)findViewById(R.id.tableview);

        initData();

        SimpleTableDataAdapter dataAdapter = new SimpleTableDataAdapter(this,getTableData(), 6);
        dataAdapter.setTextSize(18);

        TableHeaderColumnModel columnModel = new TableHeaderColumnModel(columns);
        SimpleTableHeaderAdapter headerAdapter = new SimpleTableHeaderAdapter(this,columnModel);
        headerAdapter.setTextSize(20);

        tableView.setTableAdapter(headerAdapter,dataAdapter);

        tableView.setHeaderElevation(20);
    }

    private void initData(){
        columns = new LinkedHashMap<>();
        columns.put(0,new Pair<>("今年的收成不错",2));
        columns.put(1,new Pair<>("明年的收成肯定会更好",2));
        columns.put(2,new Pair<>("为人民服务",2));
    }

    private List<TableCellData> getTableData() {
        List<TableCellData> cellDatas = new ArrayList<>();
        cellDatas.add(new TableCellData("1", 0, 0, 2, 2));
        cellDatas.add(new TableCellData("2", 0, 2, 1, 2));
        cellDatas.add(new TableCellData("21", 0, 4, 1, 2));

        cellDatas.add(new TableCellData("33", 1, 2));
        cellDatas.add(new TableCellData("4", 1, 3));
        /*cellDatas.add(new TableCellData("5", 1, 4));
        cellDatas.add(new TableCellData("6", 1, 5));*/

        cellDatas.add(new TableCellData("7", 2, 0));
        cellDatas.add(new TableCellData("8", 2, 1));
        cellDatas.add(new TableCellData("9", 2, 2));
        cellDatas.add(new TableCellData("10", 2, 3));
        /*cellDatas.add(new TableCellData("11", 2, 4, 1, 2));*/
        cellDatas.add(new TableCellData("11", 1, 4, 2, 2));

        cellDatas.add(new TableCellData("12", 3, 0));
        cellDatas.add(new TableCellData("13", 3, 1));
        cellDatas.add(new TableCellData("14", 3, 2, 1, 2));
        cellDatas.add(new TableCellData("15", 3, 4));
        cellDatas.add(new TableCellData("16", 3, 5));

        for (int i = 4; i < 20; ++i) {
            cellDatas.add(new TableCellData(String.valueOf(i * 5 - 3), i, 0));
            cellDatas.add(new TableCellData(String.valueOf(i * 5 - 2), i, 1));
            cellDatas.add(new TableCellData(String.valueOf(i * 5 - 1), i, 2, 1, 2));
            cellDatas.add(new TableCellData(String.valueOf(i * 5 + 0), i, 4));
            cellDatas.add(new TableCellData(String.valueOf(i * 5 + 1), i, 5));
        }

        return cellDatas;
    }

}


七、自定义适配器示例
自定义适配器时,只需要继承TableHeaderAdapter和TableDataAdapter,并重写TableHeaderAdapter的getHeaderView()和TableDataAdapter的addGridLayoutView()方法即可。例如,本文给出的SimpleTableHeaderAdapter和SimpleTableDataAdapter。

总结
程序的编写借鉴了SortableTableView的编码思路,通过GridLayout基本实现了跨行、列合并单元格。由于对GridLayout使用不熟悉,导致绕了很多弯路。程序代码本身比较简单,读者也可以根据自己的需求自定义自己的适配器。
程序本身在显示方面仍有如下小问题,如果某一个单元格很高且跨多行,则相同行的其他数据格最靠下的那个会拉伸(如下图)。


此时,只能通过动态计算数据格的高度得到GridLayout的最终高度,然后设置每个数据格的权重来解决(下一步优化方向)。如果不追求美观的话,则本程序完全可以使用。

源码下载
https://github.com/WJKCharlie/AndroidTableView

参考资料
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值