在Android中,要实现一个表格很容易,直接一个原生控件ListView或者GridView就行了,网上也有很多自定义TableView的思路和成品代码,在这里自己尝试使用ListView实现一个自定义的表格View,里面没有什么高大上的技术,主要是练习一些平时学习积累的小知识点并与大家分享(顺便练习一下Markdown的使用 ^ ^!),所以代码应该是很容易看懂的。
本篇主要介绍这个TableView的实现原理 (之后会有一些简单的扩展)。
首先从表格整体来看,要求能上下滑动,列数太多的时候能左右滑动,这个使用ListView和横向的HorizontalScrollView就能实现,再考虑表格有一个标题栏,最终就确定了TableView的整体布局如图所示
有了图,就可以看图写代码了
(1) 首先定义一个继承自HorizontalScrollView的类,取名TableView
public class TableView extends HorizontalScrollView {}
(2) 然后新建一个放到 HorizontalScrollView 里面的布局文件 table_view_layout.xml
(3)布局文件写好之后添加到TableView里面
View.inflate(mContext, R.layout.table_view_layout, this);
这里注意inflate的第三个参数是this,相当于用table_view_layout创建一个view,然后TableView.add(view)的效果,之后就可以在TableView里面使用 findViewById 方法取得布局里面的view了,如下:
FrameLayout mHeaderLayout = (FrameLayout) findViewById(R.id.table_header);
ListView mContentListView = (ListView) findViewById(R.id.table_content_list);
到这里TableView已经实现了图一上的布局,并且拿到了表头 mHeaderLayout 和 内容列表 mContentListView,接下来只需要往这两个里面添加内容就可以了
首先定义两个方法,添加的内容由这两个方法提供,如下代码:
// 创建表头,返回一个 LinearLayout 加到 mHeaderLayout 上
private LinearLayout createHeader() {
LinearLayout header = new LinearLayout(mContext);
header.setLayoutParams(mItemLayoutParams);
for (int i = 0; i < mColumnCount; i++) {
TextView view = new TextView(mContext);
view.setWidth(100);
view.setGravity(Gravity.CENTER_HORIZONTAL);
view.setText(mHeaderNames[i]);
view.setMaxLines(1);
view.setBackgroundResource(R.drawable.right_border);
view.setPadding(5, 10, 5, 10);
header.addView(view);
}
return header;
}
// 创建列表的item,在Adapter的getView里面用到
private LinearLayout createItem() {
LinearLayout item = new LinearLayout(mContext);
item.setLayoutParams(mItemLayoutParams);
for (int i = 0; i < mColumnCount; i++) {
item.addView(createUnitView(100));
}
return item;
}
// 这个算到创建item里面
private TextView createUnitView(int width) {
TextView view = new TextView(mContext);
view.setGravity(Gravity.CENTER);
view.setWidth(width);
view.setMaxLines(1);
view.setBackgroundResource(R.drawable.right_border);
view.setPadding(5, 10, 5, 10);
return view;
}
上面的代码 mColumnCount 表示表格的列数,mHeaderNames是显示在表头的内容,一个字符串数组,R.drawable.right_border 只有右边框的图片。
createHeader()和createItem()可以算是这个TableView的两个很重要的函数,表格样式的扩展基本围绕这两个函数来实现,这里只介绍思路,先不多说了。
然后为listView自定义一个Adapter,在Adapter的getView()方法里面使用createItem()方法创建Item。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = createItem();
}
ViewGroup itemLayout = ((ViewGroup) convertView);
String[] data = mData.get(position);
for (int i = 0; i < mColumnNum; i++) {
View childView = itemLayout.getChildAt(i);
if (childView instanceof TextView) {
((TextView) childView).setText(data[i]);
}
}
return convertView;
}
这里简单说一下我对listView优化的理解(不对的话欢迎高手指正以免误人子弟),针对ListView的优化大家都了解,一般有两点优化:
一个是判断convertView是否为空来决定是否inflate加载布局生成一个新的view,如果不是空就不inflate,也就是所说的view的复用,避免 convertView = mInflater.inflate(R.layout.item, null); 这样的代码没必要的调用。
另一个是ViewHolder,它避免的是多次调用 convertView.findViewById(R.id.tv) ,因为findViewById()是在所在的ViewGroup中从头递归查找View的,利用ViewHolder可以避免递归直接拿到所要的view。
上面getView里面的代码是用getChildAt根据索引获取需要的view的,应该是没必要使用ViewHolder来优化的。
Adapter写好之后基本ListView就完成了,然后可以随便写个函数把表头和内容列表统一加到TableView里面
private void fillTable() {
mHeaderLayout.addView(createHeader()); // 表头
//表头与列表的分割线,布局文件里面的 table_header_divider
mDividerView.setBackgroundColor(Color.parseColor("#2c2c2c"));
mDividerView.setMinimumWidth(100 * mColumnCount);
mAdapter = new TableAdapter();
mContentListView.setAdapter(mAdapter); // 内容列表
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
fillTable();
}
这里选择在view的onAttachedToWindow周期里面添加view,完成之后需要对外提供一些设置表格数据(或者一些属性)的方法,如下(看注释吧不多说了):
private List mTableData = new ArrayList<>(); //显示在列表里面的数据源,数组的list
private int mColumnCount; // 表格列数
private String[] mHeaderNames; // 表头数据
// 设置表头数据,可变参数,其实就是一个数组
public void setHeaderNames(String... names) {
mHeaderNames = names;
mColumnCount = mHeaderNames.length; // 表格列数与表头数组大小一致,先不提供set方法了
}
// 设置列表数据源
public void setTableData(List data) {
mTableData = copyData(data); //避免引用传递,看copyData方法
}
// 重载,对外支持二维数组类型的数据
public void setTableData(String[][] data) {
setTableData(Arrays.asList(data));// 转换为list,然后调用上面那个setTableData方法
}
private List copyData(List srcList) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(srcList);
String serStr = byteArrayOutputStream.toString("ISO-8859-1");
serStr = java.net.URLEncoder.encode(serStr, "UTF-8");
objectOutputStream.close();
byteArrayOutputStream.close();
String redStr = java.net.URLDecoder.decode(serStr, "UTF-8");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(redStr.getBytes("ISO-8859-1"));
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
@SuppressWarnings("unchecked")
List newList = (List) objectInputStream.readObject();
objectInputStream.close();
byteArrayInputStream.close();
return newList;
} catch (Exception e) {
Log.e(TAG, "copyData: copy list error, Exception=" + e);
}
return null;
}
关于上面的copyData方法,这是一个网上查到的序列化对象的方法,避免List对象的引用传递。List的拷贝网上有很多文章,包括深拷贝与浅拷贝,这里就不细说了,只是简单总结一下并说一下我自己的看法, List的拷贝方法,总的来说可以分为三种:
1. 直接循环遍历的方式,最不提倡的方式,太low
2. System.arraycopy()的方式,(通过底层jni实现,好像是直接复制内存),效率最高,不过是浅拷贝,一些list拷贝方式比如使用List实现类的构造方法拷贝和list.addAll()方法拷贝等最终都是调用的这个方法,都是浅拷贝
3. java.util.Collections工具类里面的copy方法,网上很多说这个是深拷贝,不过我看了下源码里面就是利用迭代器循环拷贝的,感觉应该是浅拷贝,我试了一下复制字符串数组的list,表现的现象就是浅拷贝,对于基本数据类型就不用谈深浅拷贝的问题了吧
另外还有一个实现Cloneable接口的方法我没有去研究,最终选择了上面的方法进行list的复制
================================================================
然后就是使用这个TableView了
TableView tableView = (TableView)findViewById(R.id.test_table_view);
tv.setHeaderNames("t1","t2","t3","t4","t5","t6","t7","t8","t9","t10","t11","t12");
tv.setTableData(getTestData()); //这里传入一个字符串数组的list或者字符串的二维数组
================================================================
到这里只是实现了一个基本的展示功能,列宽行高也都是写死的,不过思路就是这样,后续会填一些坑和做一些简单的扩展,扩展也就是上面说到的那样主要在createHeader()和createItem()这两个方法里面修改,Adapter可能也要改一些东西,暂时想到的有下面这些:
- 行高列宽自定义设置
- 数据的适配
- 边框线的相关设置
- 字体颜色大小
- 背景颜色
- 事件交互(item或者单元格的事件响应)
- 编辑相关(主要是行的增删改)
暂时先想这么多......
菜鸟第一次写技术文章(好像里面也没啥技术,都是一些简单的实现 ^^!),不知道思路有没有写清楚,最后源码地址,有兴趣的可以看一下
https://github.com/developerzjy/AndroidTableView
git上面有两个分支,一个是对应本篇的这个基本的原理代码,另一个是主分支,后续的扩展会随时在主分支上更新
下一篇:Android自定义TableView (二) 扩展 - 样式