重拾安卓:自定義View之表格封裝實現

今天開始更新【重拾安卓】系列文章。

因業務需要又要做一個 Android 原生的項目,記錄下時隔幾年之後再開發安卓的那些事。講的不會太基礎,基本上是自定義View封裝,複雜功能的實現等等,有需要的小夥伴可以關注~


安卓對錶格的支持不是太友好,前端很快能實現的簡單表格,安卓寫的話要費很大精力。

拿到需求之後,稍微複雜點的功能在 github 上搜一下有沒有好用的第三方框架,無疑是最節省時間的。表格還真有幾個不錯的框架,star 最多的是 smartTable ,的確很強大,只需設置數據就能自動生成表格。

但考慮各種因素還是決定自己擼一個表格,一是後端返回的數據結構還沒定,二是需求並不是太複雜,只是個簡單表格,三是找找手感~

一、需求分析及實現原理

最終效果:

實現目標:

  1. 行數不固定,超出父容器可以上下滾動
  2. 列數不固定,不管有多少列,都平分父容器寬度,每列的寬度一致
  3. 表頭設置灰色背景,單元格是白色背景

實現原理:

兩層 RecyclerView 嵌套,最外層是垂直方向的 RecyclerView,每一行是一個 item。每行又包含一個內層 RecyclerView,每行的每個單元格是內層 RecyclerViewitem

二、代碼實現

爲了方便重用,我們把這個課表封裝成自定義 View,並對外暴露一個方法設置數據。

Android 自定義 View 有三種方式:組合、擴展、重寫。我們這裏用的是組合的方式,即把已有的控件組合起來形成符合需求的自定義控件。

2.1 自定義View 主文件 StudentWorkTableView

新建一個 Java 類 StudentWorkTableView 並繼承 LinearLayout ,實現它的構造方法,就創建了一個自定義 View。

爲什麼繼承 LinearLayout ?其實繼承其他的 RelativeLayoutConstraintLayout 都可以,一般是你的 xml 最外層用的是什麼佈局,就繼承什麼。

構造方法要實現三個,因爲不同的創建方式走的構造方法不一樣,所以都要求實現。

構造方法小技巧:把前兩個參數少的構造方法裏的 super 改成 this,並填充默認值變成三個參數,就會都調用三個參數的構造方法了,業務邏輯只需寫在最後一個構造方法裏即可。

這個 View 很簡單,先在構造方法裏綁定 xml 佈局,再執行初始化方法初始數據,然後在 onLayout 中計算每個單元格的寬度,最後對外暴露一個方法設置數據。自定義 View 基本都是這個套路。

注意這裏用到了第三方框架 ButterKnife ,簡化了 findViewById ,不熟悉的同學可以查查相關資料。

代碼註釋寫的比較詳細,就不多說了直接看代碼。

public class StudentWorkTableView extends LinearLayout {

    @BindView(R.id.recycler_view_week_table)
    RecyclerView recyclerView;

    private Context mContext;

    private List<TableListModel> mList;
    private int mCellWidth;
    private StudentWorkTableAdapter mTableAdapter;

    public StudentWorkTableView(Context context) {
        this(context, null, 0);
    }

    public StudentWorkTableView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StudentWorkTableView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        View view = View.inflate(context, R.layout.view_student_work_table, this);
        ButterKnife.bind(view, this);
        mContext = context;
    }

    /**
     * 對外暴露的方法,設置表格的數據
     *
     * @param list
     */
    public void setData(List<TableListModel> list) {
        mList = list;
        init();
    }

    /**
     * 初始化方法
     */
    private void init() {
        LinearLayoutManager lm = new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false);
        recyclerView.setLayoutManager(lm);
        recyclerView.setItemAnimator(new DefaultItemAnimator());
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        // onLayout 時 View 的寬高已經確定了,可以拿到比較準確的值
        int width = getWidth();
        // 計算每列即每個單元格的寬度。用 View 總寬度除以列數就得到了每個單元格的寬度
        mCellWidth = width / mList.get(0).getTableList().size();
        if (mTableAdapter == null) {
            //把單元格寬度傳給 Adapter,在 Adapter 中對單元格重設寬度
            mTableAdapter = new StudentWorkTableAdapter(mContext, mCellWidth, R.layout.item_student_work_table_view, mList);
            recyclerView.setAdapter(mTableAdapter);
        }
    }

}

2.2 佈局文件 view_student_work_table.xml

對應的佈局文件 view_student_work_table.xml

佈局很簡單,只有一個 RecyclerView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view_week_table"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

2.3 外層 RecyclerView 的適配器 StudentWorkTableAdapter

這個適配器是控制每行的顯示。

Adapter 用到了吊炸天的 BaseRecyclerViewAdapterHelper ,節省了很多代碼。只需在 convert() 方法裏找到view 並設置數據 即可。

public class StudentWorkTableAdapter extends BaseQuickAdapter<TableListModel, BaseViewHolder> {

    private Context mContext;
    private int mCellWidth;
    private StudentWorkTableCellAdapter mCellAdapter;

    public StudentWorkTableAdapter(Context context, int cellWidth, int layoutResId, @Nullable List<TableListModel> data) {
        super(layoutResId, data);
        mContext = context;
        mCellWidth = cellWidth;
    }

    @Override
    protected void convert(BaseViewHolder helper, TableListModel item) {
        RecyclerView recyclerView = helper.getView(R.id.content_recycler_view);
        //注意這個RecyclerView要用橫向的佈局,以展示每一列
        LinearLayoutManager lm = new LinearLayoutManager(mContext, LinearLayoutManager.HORIZONTAL, false);
        recyclerView.setLayoutManager(lm);
        //設置adapter
        mCellAdapter = new StudentWorkTableCellAdapter(mContext, mCellWidth, R.layout.item_student_work_cell, item.getTableList());
        recyclerView.setAdapter(mCellAdapter);
    }
}

2.4 外層 RecyclerView 的 item 佈局文件 item_student_work_table_view.xml

外層的 item 佈局文件裏也只有一個 RecyclerView,外層 RecyclerView 用來展示行,內層 RecyclerView 用來展示列。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/content_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</android.support.constraint.ConstraintLayout>

2.5 內層 RecyclerView 的適配器 StudentWorkTableCellAdapter

這個適配器是控制每個單元格。表頭跟其他行的樣式不一樣,所以需要在數據上做個區分,這裏簡單的把表頭的數據 id 都設爲 111 了。判斷如果是表頭則改變背景樣式。

public class StudentWorkTableCellAdapter extends BaseQuickAdapter<TableTitleModel, BaseViewHolder> {

    private float mCellWidth;
    private TextView tvTitle;
    private Context mContext;

    public StudentWorkTableCellAdapter(Context context, float cellWidth, int layoutResId, @Nullable List<TableTitleModel> data) {
        super(layoutResId, data);
        mCellWidth = cellWidth;
        mContext = context;
    }

    @Override
    protected void convert(BaseViewHolder helper, TableTitleModel item) {
        tvTitle = helper.getView(R.id.tv_item_cell_table);
        tvTitle.setText(item.getName());

        ViewGroup.LayoutParams layoutParams = tvTitle.getLayoutParams();
        layoutParams.width = (int)mCellWidth;

        if (item.getId().equals("111")){
            //根據標記判斷是表頭還是普通單元格,如果是表頭就改變背景色
          tvTitle.setBackground(mContext.getResources().getDrawable(R.drawable.rect_table_title));
        }
    }
}

2.6 內層 RecyclerView 的 item 佈局文件 item_student_work_cell.xml

這是每個單元格的佈局文件,無論多複雜的佈局都可以做,這裏只放一個 TextView 演示。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    xmlns:tools="http://schemas.android.com/tools"
    >

    <TextView
        android:id="@+id/tv_item_cell_table"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="第一節"
        android:textSize="14sp"
        android:textColor="@color/text_normal"
        android:gravity="center"
        android:padding="10dp"
        android:background="@drawable/rect_table_cell"
        />

</android.support.constraint.ConstraintLayout>

2.7 其他

普通單元格的背景樣式

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle"
    >
    <solid android:color="#fff"/>
    <stroke android:color="#E0E0E0" android:width="0.5dp"/>
</shape>

表頭的背景樣式

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#f1f2f3" />
    <stroke
        android:width="0.5dp"
        android:color="#E0E0E0" />
</shape>

樣式文件放在 src/main/res/drawable 目錄下。

以上就是表格自定義 View 的實現和封裝。

三、使用

封裝完之後就是使用啦,在需要使用的頁面的 xml 佈局文件中引入封裝好的自定義 View 即可

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="20dp"
    android:orientation="vertical"
    >
    <com.solo.presentation.view.StudentWorkTableView
        android:id="@+id/work_table_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

在代碼中通過 id 找到 StudentWorkTableView,然後設置數據

@BindView(R.id.work_table_view)
StudentWorkTableView workTableView;

private List<TableListModel> tableListModels;

private void initWorkTableView() {
  //從 assets 的 json 文件中讀取數據
  String json = AssetsUtils.getJson("work_table_data.json", getActivity());
  Gson gson = new Gson();
  tableListModels = gson.fromJson(json, new TypeToken<List<TableListModel>>(){}.getType());
  //設置數據給 TableView
  workTableView.setData(tableListModels);
}

數據是通過讀取本地的 json 文件模擬的假數據,正常情況下應該請求接口獲取數據的。獲取到數據之後調用 workTableView.setData(tableListModels); 把數據設置進自定義 View 就可以啦。

附上 TableListModel 對象,get()、set() 方法省略

public class TableListModel {
    private List<TableTitleModel> tableList;
}

TableTitleModel 對象,get()、set() 方法省略

public class TableTitleModel {
    private String id;
    private String name;
}

四、延伸

如何獲取本地 json 文件的數據呢?

  1. 先建一個 assets 目錄,位置是 src/main/assets,跟 javares 平級。
  2. 在 assets 目錄下新建並編寫 json 文件
  3. 在 java 代碼中讀取 json

讀取 json 封裝成了個工具類 AssetsUtils

/**
 * 讀取 assets 文件夾中的文件工具類
 */
public class AssetsUtils {

    /**
     * 獲取assets中的json
     * @param fileName
     * @param context
     * @return
     */
    public static String getJson(String fileName, Context context){
        StringBuilder stringBuilder = new StringBuilder();
        try {
            InputStream is = context.getAssets().open(fileName);
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
            String line;
            while ((line=bufferedReader.readLine()) != null){
                stringBuilder.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return stringBuilder.toString();
    }
}

附上 json 文件

[
  {
    "tableList": [
      {
        "id": "111",
        "name": "星期一"
      },
      {
        "id": "111",
        "name": "星期二"
      },
      {
        "id": "111",
        "name": "星期三"
      },
      {
        "id": "111",
        "name": "星期四"
      },
      {
        "id": "111",
        "name": "星期五"
      },
      {
        "id": "111",
        "name": "星期六"
      },
      {
        "id": "111",
        "name": "星期日"
      }
    ]
  },
  {
    "tableList": [
      {
        "id": "11",
        "name": "小紫"
      },
      {
        "id": "12",
        "name": "小明"
      },
      {
        "id": "13",
        "name": "小紅"
      },
      {
        "id": "14",
        "name": "小綠"
      },
      {
        "id": "15",
        "name": "小黃"
      },
      {
        "id": "14",
        "name": "張三"
      },
      {
        "id": "15",
        "name": "李四"
      }
    ]
  },
  {
    "tableList": [
      {
        "id": "11",
        "name": "小紫"
      },
      {
        "id": "12",
        "name": "小明"
      },
      {
        "id": "13",
        "name": "小紅"
      },
      {
        "id": "14",
        "name": "小綠"
      },
      {
        "id": "15",
        "name": "小黃"
      },
      {
        "id": "14",
        "name": "張三"
      },
      {
        "id": "15",
        "name": "李四"
      }
    ]
  },
  {
    "tableList": [
      {
        "id": "11",
        "name": "小紫"
      },
      {
        "id": "12",
        "name": "小明"
      },
      {
        "id": "13",
        "name": "小紅"
      },
      {
        "id": "14",
        "name": "小綠"
      },
      {
        "id": "15",
        "name": "小黃"
      },
      {
        "id": "14",
        "name": "張三"
      },
      {
        "id": "15",
        "name": "李四"
      }
    ]
  },
  {
    "tableList": [
      {
        "id": "11",
        "name": "小紫"
      },
      {
        "id": "12",
        "name": "小明"
      },
      {
        "id": "13",
        "name": "小紅"
      },
      {
        "id": "14",
        "name": "小綠"
      },
      {
        "id": "15",
        "name": "小黃"
      },
      {
        "id": "14",
        "name": "張三"
      },
      {
        "id": "15",
        "name": "李四"
      }
    ]
  },
  {
    "tableList": [
      {
        "id": "11",
        "name": "小紫"
      },
      {
        "id": "12",
        "name": "小明"
      },
      {
        "id": "13",
        "name": "小紅"
      },
      {
        "id": "14",
        "name": "小綠"
      },
      {
        "id": "15",
        "name": "小黃"
      },
      {
        "id": "14",
        "name": "張三"
      },
      {
        "id": "15",
        "name": "李四"
      }
    ]
  }
]

五、下集預告

簡單的表格不過癮?再擼一個有合併單元格的複雜表頭表格吧,效果圖如下:

這基本能覆蓋大部分場景了,依然是純手擼,不用其他框架,敬請期待~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章