Giới thiệu
Ví dụ này minh họa cách thêm hàng (TableRow) động vào TableLayout bên trong ScrollView để hiển thị danh sách hóa đơn (invoice) với nhiều cột, dùng AsyncTask mô phỏng tải dữ liệu.
Bước 1: Tạo dự án mới
Tạo project Android Studio: File → New Project.
Bước 2: Layout (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
xmlns:app="https://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
android:id="@+id/invoices_layout"
tools:context=".MainActivity">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TableLayout
android:id="@+id/tableInvoices"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="0dp"
android:stretchColumns="*">
</TableLayout>
</ScrollView>
</LinearLayout>
Bước 3: Model - InvoiceData.java
package com.app.sample;
import java.math.BigDecimal;
import java.util.Date;
public class InvoiceData {
public int id;
public int invoiceNumber;
public Date invoiceDate;
public String customerName;
public String customerAddress;
public BigDecimal invoiceAmount;
public BigDecimal amountDue;
}
Bước 4: Data Source - Invoices.java
package com.app.sample;
import java.math.BigDecimal;
import java.util.Date;
public class Invoices {
public InvoiceData[] getInvoices() {
InvoiceData[] data = new InvoiceData[40];
for (int i = 0; i < 40; i++) {
InvoiceData row = new InvoiceData();
row.id = i + 1;
row.invoiceNumber = row.id;
row.amountDue = BigDecimal.valueOf(20.00 * i);
row.invoiceAmount = BigDecimal.valueOf(120.00 * (i + 1));
row.invoiceDate = new Date();
row.customerName = "Nguyễn Văn A";
row.customerAddress = "123 Đường ABC, Quận 1, TP.HCM";
data[i] = row;
}
return data;
}
}
Bước 5: MainActivity - Tạo TableRow động
Tạo các TextView, LinearLayout con cho mỗi cột, add vào TableRow, sau đó add vào TableLayout. Có dòng phân cách (separator) giữa các hàng.
package com.app.sample;
import androidx.appcompat.app.AppCompatActivity;
import android.app.ProgressDialog;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
public class MainActivity extends AppCompatActivity {
private TableLayout mTableLayout;
ProgressDialog mProgressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mProgressBar = new ProgressDialog(this);
mTableLayout = findViewById(R.id.tableInvoices);
mTableLayout.setStretchAllColumns(true);
startLoadData();
}
public void startLoadData() {
mProgressBar.setCancelable(false);
mProgressBar.setMessage("Đang tải hóa đơn...");
mProgressBar.setProgressStyle(ProgressDialog.STYLE_SPINNER);
mProgressBar.show();
new LoadDataTask().execute(0);
}
public void loadData() {
int textSize = (int) getResources().getDimension(android.R.dimen.text_size_small); // Thay bằng dimen tự định nghĩa
int smallTextSize = (int) getResources().getDimension(android.R.dimen.text_size_small);
int mediumTextSize = (int) getResources().getDimension(android.R.dimen.text_size_medium);
Invoices invoices = new Invoices();
InvoiceData[] data = invoices.getInvoices();
SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMM, yyyy", Locale.getDefault());
DecimalFormat decimalFormat = new DecimalFormat("0.00");
int rows = data.length;
getSupportActionBar().setTitle("Hóa đơn (" + rows + ")");
mTableLayout.removeAllViews();
// Hàng tiêu đề (i = -1)
for (int i = -1; i < rows; i++) {
InvoiceData row = (i > -1) ? data[i] : null;
// Cột 1: Số hóa đơn
TextView tv = new TextView(this);
tv.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT));
tv.setGravity(Gravity.LEFT);
tv.setPadding(5, 15, 0, 15);
if (i == -1) {
tv.setText("Số HĐ");
tv.setBackgroundColor(Color.parseColor("#f0f0f0"));
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, smallTextSize);
} else {
tv.setBackgroundColor(Color.parseColor("#f8f8f8"));
tv.setText(String.valueOf(row.invoiceNumber));
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
}
// Cột 2: Ngày
TextView tv2 = new TextView(this);
if (i == -1) {
tv2.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT));
tv2.setTextSize(TypedValue.COMPLEX_UNIT_PX, smallTextSize);
} else {
tv2.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.MATCH_PARENT));
tv2.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
}
tv2.setGravity(Gravity.LEFT);
tv2.setPadding(5, 15, 0, 15);
if (i == -1) {
tv2.setText("Ngày");
tv2.setBackgroundColor(Color.parseColor("#f7f7f7"));
} else {
tv2.setBackgroundColor(Color.WHITE);
tv2.setTextColor(Color.BLACK);
tv2.setText(dateFormat.format(row.invoiceDate));
}
// Cột 3: Khách hàng (LinearLayout dọc: tên + địa chỉ)
LinearLayout layCustomer = new LinearLayout(this);
layCustomer.setOrientation(LinearLayout.VERTICAL);
layCustomer.setPadding(0, 10, 0, 10);
layCustomer.setBackgroundColor(Color.parseColor("#f8f8f8"));
TextView tv3 = new TextView(this);
if (i == -1) {
tv3.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.MATCH_PARENT));
tv3.setPadding(5, 5, 0, 5);
tv3.setTextSize(TypedValue.COMPLEX_UNIT_PX, smallTextSize);
} else {
tv3.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.MATCH_PARENT));
tv3.setPadding(5, 0, 0, 5);
tv3.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
}
tv3.setGravity(Gravity.TOP);
if (i == -1) {
tv3.setText("Khách hàng");
tv3.setBackgroundColor(Color.parseColor("#f0f0f0"));
} else {
tv3.setBackgroundColor(Color.parseColor("#f8f8f8"));
tv3.setTextColor(Color.BLACK);
tv3.setTextSize(TypedValue.COMPLEX_UNIT_PX, smallTextSize);
tv3.setText(row.customerName);
}
layCustomer.addView(tv3);
if (i > -1) {
TextView tv3b = new TextView(this);
tv3b.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT));
tv3b.setGravity(Gravity.RIGHT);
tv3b.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
tv3b.setPadding(5, 1, 0, 5);
tv3b.setTextColor(Color.parseColor("#aaaaaa"));
tv3b.setBackgroundColor(Color.parseColor("#f8f8f8"));
tv3b.setText(row.customerAddress);
layCustomer.addView(tv3b);
}
// Cột 4: Số tiền (LinearLayout dọc: tổng + công nợ)
LinearLayout layAmounts = new LinearLayout(this);
layAmounts.setOrientation(LinearLayout.VERTICAL);
layAmounts.setGravity(Gravity.RIGHT);
layAmounts.setPadding(0, 10, 0, 10);
layAmounts.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.MATCH_PARENT));
TextView tv4 = new TextView(this);
if (i == -1) {
tv4.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.MATCH_PARENT));
tv4.setPadding(5, 5, 1, 5);
layAmounts.setBackgroundColor(Color.parseColor("#f7f7f7"));
} else {
tv4.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT));
tv4.setPadding(5, 0, 1, 5);
layAmounts.setBackgroundColor(Color.WHITE);
}
tv4.setGravity(Gravity.RIGHT);
if (i == -1) {
tv4.setText("Tổng tiền");
tv4.setBackgroundColor(Color.parseColor("#f7f7f7"));
tv4.setTextSize(TypedValue.COMPLEX_UNIT_PX, smallTextSize);
} else {
tv4.setBackgroundColor(Color.WHITE);
tv4.setTextColor(Color.BLACK);
tv4.setText(decimalFormat.format(row.invoiceAmount));
tv4.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
}
layAmounts.addView(tv4);
if (i > -1) {
TextView tv4b = new TextView(this);
tv4b.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT));
tv4b.setGravity(Gravity.RIGHT);
tv4b.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
tv4b.setPadding(2, 2, 1, 5);
tv4b.setTextColor(Color.parseColor("#00afff"));
tv4b.setBackgroundColor(Color.WHITE);
String due = "";
if (row.amountDue.compareTo(BigDecimal.valueOf(0.01)) == 1) {
due = "Còn nợ: " + decimalFormat.format(row.amountDue);
}
tv4b.setText(due);
layAmounts.addView(tv4b);
}
// Tạo TableRow và add các cột
TableRow tr = new TableRow(this);
tr.setId(i + 1);
TableLayout.LayoutParams trParams = new TableLayout.LayoutParams(TableLayout.LayoutParams.MATCH_PARENT, TableLayout.LayoutParams.WRAP_CONTENT);
tr.setPadding(0, 0, 0, 0);
tr.setLayoutParams(trParams);
tr.addView(tv);
tr.addView(tv2);
tr.addView(layCustomer);
tr.addView(layAmounts);
if (i > -1) {
tr.setOnClickListener(v -> {
TableRow clickedRow = (TableRow) v;
// Xử lý click hàng
});
}
mTableLayout.addView(tr, trParams);
// Dòng phân cách
if (i > -1) {
TableRow trSep = new TableRow(this);
TableLayout.LayoutParams trParamsSep = new TableLayout.LayoutParams(TableLayout.LayoutParams.MATCH_PARENT, TableLayout.LayoutParams.WRAP_CONTENT);
trSep.setLayoutParams(trParamsSep);
TextView tvSep = new TextView(this);
TableRow.LayoutParams tvSepLay = new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT);
tvSepLay.span = 4; // Chiếm 4 cột
tvSep.setLayoutParams(tvSepLay);
tvSep.setBackgroundColor(Color.parseColor("#d9d9d9"));
tvSep.setHeight(1);
trSep.addView(tvSep);
mTableLayout.addView(trSep, trParamsSep);
}
}
}
class LoadDataTask extends AsyncTask {
@Override
protected String doInBackground(Integer... params) {
try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
return "Hoàn thành";
}
@Override
protected void onPostExecute(String result) {
mProgressBar.hide();
loadData();
}
@Override protected void onPreExecute() {}
@Override protected void onProgressUpdate(Integer... values) {}
}
}
Bước 6: Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android" package="com.app.sample">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Chạy ứng dụng
Chạy app, ProgressDialog hiện 2 giây, sau đó TableLayout hiển thị 40 hàng hóa đơn có thể cuộn, có tiêu đề, dòng phân cách, và click được từng hàng.
Khuyến nghị hiện đại: RecyclerView
TableLayout thêm row động phức tạp, kém hiệu năng với dữ liệu lớn. Nên dùng RecyclerView với GridLayoutManager (spanCount = số cột) hoặc LinearLayoutManager + layout item phẳng. RecyclerView hỗ trợ view recycling, animation, diffutil, pagination tự nhiên.