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

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

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

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

图一 TableView的布局

有了图,就可以看图写代码了
(1) 首先定义一个继承自HorizontalScrollView的类,取名TableView
public class TableView extends HorizontalScrollView {}

(2) 然后新建一个放到 HorizontalScrollView 里面的布局文件 table_view_layout.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/table_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >


    <FrameLayout
        android:id="@+id/table_header"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <View
        android:id="@+id/table_header_divider"
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:layout_below="@id/table_header"
        android:background="#2c2c2c" />

    <ListView
        android:id="@+id/table_content_list"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_below="@id/table_header_divider"
        android:divider="#2c2c2c"
        android:dividerHeight="1px"
        android:fadeScrollbars="false"
        android:scrollbars="none" />

</RelativeLayout>

(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<String[]> 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<String[]> data) {
        mTableData = copyData(data); //避免引用传递,看copyData方法
    }

    // 重载,对外支持二维数组类型的数据
    public void setTableData(String[][] data) {
        setTableData(Arrays.asList(data));// 转换为list,然后调用上面那个setTableData方法
    }
    
private List<String[]> copyData(List<String[]> 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<String[]> newList = (List<String[]>) 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或者字符串的二维数组
图2 实现的效果图

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

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容