Android自定義TableView (一) 原理介紹

在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的整體布局如圖所示

圖一 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ù)組
圖2 實(shí)現(xiàn)的效果圖

================================================================
到這里只是實(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í)在主分支上更新

下一篇:Android自定義TableView (二) 擴(kuò)展 - 樣式

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容