Hiển thị bảng và tự động lặp trên ListView trong Android: Ví dụ chi tiết

Trong phát triển ứng dụng Android, đôi khi chúng ta cần hiển thị dữ liệu dưới dạng bảng tương tự như HTML. Android không cung cấp trực tiếp một điều khiển bảng, nhưng có thể đạt được kết quả mong muốn thông qua các phương pháp khác. Đối với dữ liệu cố định và các ô có chiều rộng bằng nhau, chúng ta có thể sử dụng GridView. Tuy nhiên, khi tập dữ liệu không xác định và các ô có thể co giãn, chúng ta nên kết hợp TableLayout và ListView.

1. Vấn đề chiều rộng bảng

TableLayout có hai thuộc tính shrinkColumns (tự động co giãn) và stretchColumns (tự động mở rộng). Ví dụ: android:shrinkColumns="1,3,7" android:stretchColumns="1,3,7" có nghĩa là các cột 1,3,7 sẽ tự động co giãn/mở rộng, với số thứ tự cột bắt đầu từ 0. Điều này giúp điều chỉnh hiển thị theo độ rộng màn hình. Khi màn hình không đủ rộng, nội dung sẽ tự động xuống dòng. Nếu đặt là "*" thì áp dụng cho tất cả các cột.

Tuy nhiên, chỉ thiết lập các thuộc tính này vẫn chưa đủ. Lý do là nội dung trong các ô có độ dài khác nhau, và tiêu đề cũng có độ dài khác nhau. Nếu tất cả các ô đều tự động điều chỉnh chiều rộng, các cột sẽ không căn chỉnh đúng, làm cho các đường phân cách bị lệch. Để căn chỉnh các cột, chúng ta cần chỉ định chiều rộng cố định thay vì tự động hoặc wrap_content/match_parent.

Vì độ phân giải màn hình điện thoại đa dạng, việc thiết lập chiều rộng cố định cần được tính toán trong code, không nên cứng nhắc trong tệp xml.

2. Vấn đề đường viền bảng (đường phân cách)

Việc triển khai đường phân cách giữa các ô khá đơn giản, chỉ cần sử dụng một đường với chiều rộng 1dp và màu sắc mong muốn. Ví dụ: <View android:layout_width="1dp" android:layout_height="match_parent" android:background="#F00"/>

3. styles.xml

Để tái sử dụng code, chúng ta trích xuất một số thuộc tính vào tệp styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Đường phân cách -->
    <style name="list_item_seperator_layout">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">1dp</item>
        <item name="android:background">@android:color/holo_green_light</item>
    </style>

    <style name="list_item_cell_seperator_layout">
        <item name="android:layout_width">1dp</item>
        <item name="android:layout_height">match_parent</item>
        <item name="android:background">@android:color/holo_green_light</item>
    </style>
    <!-- Kiểu chữ -->
    <style name="textViewHead">
        <item name="android:textSize">30sp</item>
        <item name="android:textStyle">bold</item>
        <item name="android:textColor">#F00</item>
        <item name="android:gravity">center</item>
    </style>
    <style name="textViewCell">
        <item name="android:textSize">27sp</item>
        <item name="android:textColor">#F00</item>
        <item name="android:gravity">center</item>
    </style>
</resources>
4. activity_main.xml - Bố cục bảng
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp"
    tools:context=".MainActivity">

    <View style="@style/list_item_seperator_layout" />

    <TableLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:shrinkColumns="7"
        android:stretchColumns="7">
        <TableRow
            android:id="@+id/stock_list_header_row"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <View style="@style/list_item_cell_seperator_layout" />
            <TextView
                android:id="@+id/head1"
                style="@style/textViewHead"
                android:text="Thời gian khởi hành" />
            <View style="@style/list_item_cell_seperator_layout" />
            <TextView
                android:id="@+id/head2"
                style="@style/textViewHead"
                android:text="Biển số xe" />
            <View style="@style/list_item_cell_seperator_layout" />
            <TextView
                android:id="@+id/head3"
                style="@style/textViewHead"
                android:text="Quãng đường" />
            <View style="@style/list_item_cell_seperator_layout" />
            <TextView
                android:id="@+id/head4"
                style="@style/textViewHead"
                android:text="Điểm đến" />
            <View style="@style/list_item_cell_seperator_layout" />
        </TableRow>
    </TableLayout>
    <View style="@style/list_item_seperator_layout"
        android:layout_height="2dp"
        />
    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="@android:color/holo_green_light"
        android:dividerHeight="1dp" />
</LinearLayout>

Chiều rộng của các cột trong tiêu đề bảng cần được chỉ định trong code, với cột cuối cùng tự động điều chỉnh. Chiều rộng các cột trong tiêu đề phải khớp với chiều rộng các ô trong dữ liệu.

5. item_regular.xml - Bố cục cho hàng danh sách

Chúng ta sử dụng LinearLayout theo phương ngang để biểu diễn hàng danh sách, mỗi cột đặt một View làm đường phân cách. Chiều rộng các cột cần được thiết lập lại trong code (trong Adapter).

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:descendantFocusability="blocksDescendants"
    android:orientation="horizontal"
    tools:context=".adapter.DanhSachXeAdapter">
    <View style="@style/list_item_cell_seperator_layout" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/regularKhoiHanh"
        style="@style/textViewCell"
        android:text="text1" />
    <View style="@style/list_item_cell_seperator_layout" />

    <TextView
        android:id="@+id/regularBienSo"
        style="@style/textViewCell"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:text="text12" />
    <View style="@style/list_item_cell_seperator_layout" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/regularQuangDuong"
        style="@style/textViewCell"
        android:text="text13" />
    <View style="@style/list_item_cell_seperator_layout" />
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:id="@+id/regularDiemDen"
        style="@style/textViewCell"
        android:text="text14" />
    <View style="@style/list_item_cell_seperator_layout" />
</LinearLayout>

Thực tế, tiêu đề bảng cũng có thể được triển khai trực tiếp bằng LinearLayout mà không cần TableLayout. Chiều rộng các cột trong tiêu đề phải khớp với chiều rộng các ô trong dữ liệu.

6. DuLieuXe.java - Lớp dữ liệu
package com.example.listviewapplication.entity;

public class DuLieuXe {
    private String bienSo;
    private String thoiGianKhoiHanh;
    private Integer quangDuong;
    private String diemDenCuoi;

    public DuLieuXe() {
    }

    public DuLieuXe(String bienSo, String thoiGianKhoiHanh, Integer quangDuong, String diemDenCuoi) {
        this.bienSo = bienSo;
        this.thoiGianKhoiHanh = thoiGianKhoiHanh;
        this.quangDuong = quangDuong;
        this.diemDenCuoi = diemDenCuoi;
    }

    public String getBienSo() {
        return bienSo;
    }

    public void setBienSo(String bienSo) {
        this.bienSo = bienSo;
    }

    public String getThoiGianKhoiHanh() {
        return thoiGianKhoiHanh;
    }

    public void setThoiGianKhoiHanh(String thoiGianKhoiHanh) {
        this.thoiGianKhoiHanh = thoiGianKhoiHanh;
    }

    public Integer getQuangDuong() {
        return quangDuong;
    }

    public void setQuangDuong(Integer quangDuong) {
        this.quangDuong = quangDuong;
    }

    public String getDiemDenCuoi() {
        return diemDenCuoi;
    }

    public void setDiemDenCuoi(String diemDenCuoi) {
        this.diemDenCuoi = diemDenCuoi;
    }
}
7. DanhSachXeAdapter.java - Adapter cho danh sách
package com.example.listviewapplication;

import android.content.Context;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

import com.example.listviewapplication.entity.DuLieuXe;

import java.util.List;

public class DanhSachXeAdapter extends BaseAdapter {
    private LayoutInflater layoutInflater;
    private List<DuLieuXe> danhSachXe;
    private DisplayMetrics displayMetrics ;

    public DanhSachXeAdapter(Context context, List<DuLieuXe> danhSachXe) {
        layoutInflater = (LayoutInflater)context.getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);
        displayMetrics = context.getResources().getDisplayMetrics();
        this.danhSachXe = danhSachXe;
    }

    @Override
    public int getCount() {
        if(danhSachXe == null) return 0;
        return danhSachXe.size();
    }

    @Override
    public Object getItem(int position) {
        return danhSachXe.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view;
        if (convertView == null) {
            view = layoutInflater.inflate(R.layout.item_regular, parent, false);
        } else {
            view = convertView;
        }
        
        DuLieuXe item = (DuLieuXe) getItem(position);
        TextView tvBienSo = (TextView) view.findViewById(R.id.regularBienSo);
        TextView tvKhoiHanh = (TextView) view.findViewById(R.id.regularKhoiHanh);
        TextView tvQuangDuong = (TextView) view.findViewById(R.id.regularQuangDuong);
        TextView tvDiemDen = (TextView) view.findViewById(R.id.regularDiemDen);

        // Thiết lập chiều rộng cho các TextView
        int cellWidth = displayMetrics.widthPixels / 4;
        tvBienSo.setWidth(cellWidth);
        tvKhoiHanh.setWidth(cellWidth);
        tvQuangDuong.setWidth(cellWidth);
        tvDiemDen.setWidth(cellWidth);

        // Thiết lập nội dung
        tvBienSo.setText(item.getBienSo());
        tvKhoiHanh.setText(item.getThoiGianKhoiHanh());
        tvQuangDuong.setText(String.valueOf(item.getQuangDuong()));
        tvDiemDen.setText(item.getDiemDenCuoi());

        return view;
    }
}

Đoạn code quan trọng là thiết lập chiều rộng cho 4 TextView.

8. Tính năng tự động lặp

Sử dụng phương thức post của Handler và phương thức setSelection của ListView để thực hiện tính năng tự động lặp. Lưu ý cần loại bỏ Runnable khỏi handler khi hủy hoạt động.

@Override
    protected void onDestroy() {
        super.onDestroy();
        handler.removeCallbacks(autoScrollRunnable);
    }
    
    boolean isScrollToEnd = false;
    Handler handler = new Handler();
    Runnable autoScrollRunnable = new Runnable() {
        @Override
        public void run() {
            // Tính toán vị trí cuộn
            int currentPosition = isScrollToEnd ? 0 : listView.getLastVisiblePosition();
            listView.setSelection(currentPosition);
            Log.e("AUTO_SCROLL", "Vị trí hiện tại: " + currentPosition);
            isScrollToEnd = currentPosition >= danhSachXe.size() - 1;
            handler.postDelayed(this, AUTO_SCROLL_INTERVAL);
        }
    };
9. MainActivity.java
package com.example.listviewapplication;

import android.app.ListActivity;
import android.os.Bundle;
import android.widget.ListView;
import android.os.Handler;
import android.util.DisplayMetrics;
import android.util.Log;
import android.widget.ListAdapter;
import android.widget.TextView;

import com.example.listviewapplication.entity.DuLieuXe;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends ListActivity {
    private ListView listView;
    private List<DuLieuXe> danhSachXe;
    private final static int AUTO_SCROLL_INTERVAL = 3 * 1000; // 3 giây
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();

        // Thiết lập chiều rộng cho tiêu đề
        ((TextView)findViewById(R.id.head1)).setWidth(displayMetrics.widthPixels/4);
        ((TextView)findViewById(R.id.head2)).setWidth(displayMetrics.widthPixels/4);
        ((TextView)findViewById(R.id.head3)).setWidth(displayMetrics.widthPixels/4);
        ((TextView)findViewById(R.id.head4)).setWidth(displayMetrics.widthPixels/4);

        // Tạo dữ liệu mẫu
        danhSachXe = new ArrayList<>();
        for(int i = 0; i < 30; i++) {
            DuLieuXe xe = new DuLieuXe();
            xe.setBienSo(i % 3 == 0 ? "琼B" + i : "琼Abcd" + i);
            xe.setThoiGianKhoiHanh("15:30");
            xe.setQuangDuong(35 + i);
            xe.setDiemDenCuoi(i % 4 == 0 ? "儋州" + i : "三亚海口文昌" + i);
            danhSachXe.add(xe);
        }
        
        // Thiết lập adapter
        ListAdapter adapter = new DanhSachXeAdapter(this, danhSachXe);
        setListAdapter(adapter);
        
        Log.e("SCREEN_INFO", displayMetrics.widthPixels + "," + 
              displayMetrics.heightPixels + "," + displayMetrics.widthPixels/4);
              
        listView = (ListView) findViewById(android.R.id.list);
        
        // Bắt đầu tự động cuộn
        handler.postDelayed(autoScrollRunnable, AUTO_SCROLL_INTERVAL);
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        handler.removeCallbacks(autoScrollRunnable);
    }
    
    boolean isScrollToEnd = false;
    Handler handler = new Handler();
    Runnable autoScrollRunnable = new Runnable() {
        @Override
        public void run() {
            // Tính toán vị trí cuộn
            int currentPosition = isScrollToEnd ? 0 : listView.getLastVisiblePosition();
            listView.setSelection(currentPosition);
            Log.e("AUTO_SCROLL", "Vị trí hiện tại: " + currentPosition);
            isScrollToEnd = currentPosition >= danhSachXe.size() - 1;
            handler.postDelayed(this, AUTO_SCROLL_INTERVAL);
        }
    };
}

Đoạn code quan trọng là thiết lập chiều rộng cho các TextView trong tiêu đề, trên đây là toàn bộ code ví dụ.

Thẻ: Android ListView TableLayout Adapter Handler

Đăng vào ngày 16 tháng 6 lúc 06:12