今天開始更新【重拾安卓】系列文章。php
因業務須要又要作一個 Android 原生的項目,記錄下時隔幾年以後再開發安卓的那些事。講的不會太基礎,基本上是自定義View封裝,複雜功能的實現等等,有須要的小夥伴能夠關注~前端
安卓對錶格的支持不是太友好,前端很快能實現的簡單表格,安卓寫的話要費很大精力。java
拿到需求以後,稍微複雜點的功能在 github 上搜一下有沒有好用的第三方框架,無疑是最節省時間的。表格還真有幾個不錯的框架,star 最多的是 smartTable ,的確很強大,只需設置數據就能自動生成表格。android
但考慮各類因素仍是決定本身擼一個表格,一是後端返回的數據結構還沒定,二是需求並非太複雜,只是個簡單表格,三是找找手感~git
最終效果:github
實現目標:json
實現原理:後端
兩層 RecyclerView
嵌套,最外層是垂直方向的 RecyclerView
,每一行是一個 item
。每行又包含一個內層 RecyclerView
,每行的每一個單元格是內層 RecyclerView
的 item
。數據結構
爲了方便重用,咱們把這個課表封裝成自定義 View,並對外暴露一個方法設置數據。app
Android 自定義 View 有三種方式:組合、擴展、重寫。咱們這裏用的是組合的方式,即把已有的控件組合起來造成符合需求的自定義控件。
新建一個 Java 類 StudentWorkTableView
並繼承 LinearLayout
,實現它的構造方法,就建立了一個自定義 View。
爲何繼承 LinearLayout
?其實繼承其餘的 RelativeLayout
、ConstraintLayout
均可以,通常是你的 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);
}
}
}
複製代碼
對應的佈局文件 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>
複製代碼
這個適配器是控制每行的顯示。
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);
}
}
複製代碼
外層的 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>
複製代碼
這個適配器是控制每一個單元格。表頭跟其餘行的樣式不同,因此須要在數據上作個區分,這裏簡單的把表頭的數據 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));
}
}
}
複製代碼
這是每一個單元格的佈局文件,不管多複雜的佈局均可以作,這裏只放一個 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>
複製代碼
普通單元格的背景樣式
<?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 文件的數據呢?
src/main/assets
,跟 java
和 res
平級。讀取 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": "李四"
}
]
}
]
複製代碼
簡單的表格不過癮?再擼一個有合併單元格的複雜表頭表格吧,效果圖以下:
這基本能覆蓋大部分場景了,依然是純手擼,不用其餘框架,敬請期待~