Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Android

Thêm dòng (row) động vào TableLayout trên Android

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.