在Android中,要實(shí)現(xiàn)一個(gè)表格很容易,直接一個(gè)原生控件ListView或者GridView就行了,網(wǎng)上也有很多自定義TableView的思路和成品代碼,在這里自己嘗試使用ListView實(shí)現(xiàn)一個(gè)自定義的表格View,里面沒有什么高大上的技術(shù),主要是練習(xí)一些平時(shí)學(xué)習(xí)積累的小知識(shí)點(diǎn)并與大家分享(順便練習(xí)一下Markdown的使用 ^ ^!),所以代碼應(yīng)該是很容易看懂的。
本篇主要介紹這個(gè)TableView的實(shí)現(xiàn)原理 (之后會(huì)有一些簡單的擴(kuò)展)。
首先從表格整體來看,要求能上下滑動(dòng),列數(shù)太多的時(shí)候能左右滑動(dòng),這個(gè)使用ListView和橫向的HorizontalScrollView就能實(shí)現(xiàn),再考慮表格有一個(gè)標(biāo)題欄,最終就確定了TableView的整體布局如圖所示

有了圖,就可以看圖寫代碼了
(1) 首先定義一個(gè)繼承自HorizontalScrollView的類,取名TableView
public class TableView extends HorizontalScrollView {}
(2) 然后新建一個(gè)放到 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的第三個(gè)參數(shù)是this,相當(dāng)于用table_view_layout創(chuàng)建一個(gè)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已經(jīng)實(shí)現(xiàn)了圖一上的布局,并且拿到了表頭 mHeaderLayout 和 內(nèi)容列表 mContentListView,接下來只需要往這兩個(gè)里面添加內(nèi)容就可以了
首先定義兩個(gè)方法,添加的內(nèi)容由這兩個(gè)方法提供,如下代碼:
// 創(chuàng)建表頭,返回一個(gè) 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;
}
// 創(chuàng)建列表的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;
}
// 這個(gè)算到創(chuàng)建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 表示表格的列數(shù),mHeaderNames是顯示在表頭的內(nèi)容,一個(gè)字符串?dāng)?shù)組,R.drawable.right_border 只有右邊框的圖片。
createHeader()和createItem()可以算是這個(gè)TableView的兩個(gè)很重要的函數(shù),表格樣式的擴(kuò)展基本圍繞這兩個(gè)函數(shù)來實(shí)現(xiàn),這里只介紹思路,先不多說了。
然后為listView自定義一個(gè)Adapter,在Adapter的getView()方法里面使用createItem()方法創(chuàng)建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優(yōu)化的理解(不對的話歡迎高手指正以免誤人子弟),針對ListView的優(yōu)化大家都了解,一般有兩點(diǎn)優(yōu)化:
一個(gè)是判斷convertView是否為空來決定是否inflate加載布局生成一個(gè)新的view,如果不是空就不inflate,也就是所說的view的復(fù)用,避免 convertView = mInflater.inflate(R.layout.item, null); 這樣的代碼沒必要的調(diào)用。
另一個(gè)是ViewHolder,它避免的是多次調(diào)用 convertView.findViewById(R.id.tv) ,因?yàn)閒indViewById()是在所在的ViewGroup中從頭遞歸查找View的,利用ViewHolder可以避免遞歸直接拿到所要的view。
上面getView里面的代碼是用getChildAt根據(jù)索引獲取需要的view的,應(yīng)該是沒必要使用ViewHolder來優(yōu)化的。
Adapter寫好之后基本ListView就完成了,然后可以隨便寫個(gè)函數(shù)把表頭和內(nèi)容列表統(tǒng)一加到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); // 內(nèi)容列表
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
fillTable();
}
這里選擇在view的onAttachedToWindow周期里面添加view,完成之后需要對外提供一些設(shè)置表格數(shù)據(jù)(或者一些屬性)的方法,如下(看注釋吧不多說了):
private List<String[]> mTableData = new ArrayList<>(); //顯示在列表里面的數(shù)據(jù)源,數(shù)組的list
private int mColumnCount; // 表格列數(shù)
private String[] mHeaderNames; // 表頭數(shù)據(jù)
// 設(shè)置表頭數(shù)據(jù),可變參數(shù),其實(shí)就是一個(gè)數(shù)組
public void setHeaderNames(String... names) {
mHeaderNames = names;
mColumnCount = mHeaderNames.length; // 表格列數(shù)與表頭數(shù)組大小一致,先不提供set方法了
}
// 設(shè)置列表數(shù)據(jù)源
public void setTableData(List<String[]> data) {
mTableData = copyData(data); //避免引用傳遞,看copyData方法
}
// 重載,對外支持二維數(shù)組類型的數(shù)據(jù)
public void setTableData(String[][] data) {
setTableData(Arrays.asList(data));// 轉(zhuǎn)換為list,然后調(diào)用上面那個(gè)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;
}
關(guān)于上面的copyData方法,這是一個(gè)網(wǎng)上查到的序列化對象的方法,避免List對象的引用傳遞。List的拷貝網(wǎng)上有很多文章,包括深拷貝與淺拷貝,這里就不細(xì)說了,只是簡單總結(jié)一下并說一下我自己的看法, List的拷貝方法,總的來說可以分為三種:
1. 直接循環(huán)遍歷的方式,最不提倡的方式,太low
2. System.arraycopy()的方式,(通過底層jni實(shí)現(xiàn),好像是直接復(fù)制內(nèi)存),效率最高,不過是淺拷貝,一些list拷貝方式比如使用List實(shí)現(xiàn)類的構(gòu)造方法拷貝和list.addAll()方法拷貝等最終都是調(diào)用的這個(gè)方法,都是淺拷貝
3. java.util.Collections工具類里面的copy方法,網(wǎng)上很多說這個(gè)是深拷貝,不過我看了下源碼里面就是利用迭代器循環(huán)拷貝的,感覺應(yīng)該是淺拷貝,我試了一下復(fù)制字符串?dāng)?shù)組的list,表現(xiàn)的現(xiàn)象就是淺拷貝,對于基本數(shù)據(jù)類型就不用談深淺拷貝的問題了吧
另外還有一個(gè)實(shí)現(xiàn)Cloneable接口的方法我沒有去研究,最終選擇了上面的方法進(jìn)行l(wèi)ist的復(fù)制
================================================================
然后就是使用這個(gè)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()); //這里傳入一個(gè)字符串?dāng)?shù)組的list或者字符串的二維數(shù)組

================================================================
到這里只是實(shí)現(xiàn)了一個(gè)基本的展示功能,列寬行高也都是寫死的,不過思路就是這樣,后續(xù)會(huì)填一些坑和做一些簡單的擴(kuò)展,擴(kuò)展也就是上面說到的那樣主要在createHeader()和createItem()這兩個(gè)方法里面修改,Adapter可能也要改一些東西,暫時(shí)想到的有下面這些:
- 行高列寬自定義設(shè)置
- 數(shù)據(jù)的適配
- 邊框線的相關(guān)設(shè)置
- 字體顏色大小
- 背景顏色
- 事件交互(item或者單元格的事件響應(yīng))
- 編輯相關(guān)(主要是行的增刪改)
暫時(shí)先想這么多......
菜鳥第一次寫技術(shù)文章(好像里面也沒啥技術(shù),都是一些簡單的實(shí)現(xiàn) ^^!),不知道思路有沒有寫清楚,最后源碼地址,有興趣的可以看一下
https://github.com/developerzjy/AndroidTableView
git上面有兩個(gè)分支,一個(gè)是對應(yīng)本篇的這個(gè)基本的原理代碼,另一個(gè)是主分支,后續(xù)的擴(kuò)展會(huì)隨時(shí)在主分支上更新