Android自定义TableView (一) 原理介绍

在Android中,要实现一个表格很容易,直接一个原生控件ListView或者GridView就行了,网上也有很多自定义TableView的思路和成品代码,在这里自己尝试使用ListView实现一个自定义的表格View,里面没有什么高大上的技术,主要是练习一些平时学习积累的小知识点并与大家分享(顺便练习一下Markdown的使用 ^ ^!),所以代码应该是很容易看懂的。

本篇主要介绍这个TableView的实现原理 (之后会有一些简单的扩展)。

首先从表格整体来看,要求能上下滑动,列数太多的时候能左右滑动,这个使用ListView和横向的HorizontalScrollView就能实现,再考虑表格有一个标题栏,最终就确定了TableView的整体布局如图所示

Android自定义TableView (一) 原理介绍_第1张图片
图一 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或者字符串的二维数组
Android自定义TableView (一) 原理介绍_第2张图片
图2 实现的效果图

================================================================
到这里只是实现了一个基本的展示功能,列宽行高也都是写死的,不过思路就是这样,后续会填一些坑和做一些简单的扩展,扩展也就是上面说到的那样主要在createHeader()和createItem()这两个方法里面修改,Adapter可能也要改一些东西,暂时想到的有下面这些:

  • 行高列宽自定义设置
  • 数据的适配
  • 边框线的相关设置
  • 字体颜色大小
  • 背景颜色
  • 事件交互(item或者单元格的事件响应)
  • 编辑相关(主要是行的增删改)
    暂时先想这么多......

菜鸟第一次写技术文章(好像里面也没啥技术,都是一些简单的实现 ^^!),不知道思路有没有写清楚,最后源码地址,有兴趣的可以看一下
https://github.com/developerzjy/AndroidTableView
git上面有两个分支,一个是对应本篇的这个基本的原理代码,另一个是主分支,后续的扩展会随时在主分支上更新

下一篇:Android自定义TableView (二) 扩展 - 样式

你可能感兴趣的:(Android自定义TableView (一) 原理介绍)