Trang chủHướng dẫnVí dụ về ExpandableListView trong Android
Android

Ví dụ về ExpandableListView trong Android

CyStack blog 6 phút để đọc
CyStack blog23/10/2025
Locker Avatar

Chris Pham

Technical Writer

Locker logo social
Reading Time: 6 minutes

Trong bài blog này, chúng ta sẽ cùng nhau đi sâu vào cách triển khai một ExpandableListView từ đầu đến cuối, hiểu rõ cấu trúc và code mẫu để bạn có thể tự tin áp dụng ngay vào dự án của mình. Chúng ta sẽ xây dựng một ví dụ thực tế, từ việc thiết lập layout, tạo dữ liệu mẫu cho đến việc viết Adapter tùy chỉnh và xử lý các sự kiện tương tác.

Khám phá Android ExpandableListView

ExpandableListView trong Android là một View đặc biệt, cho phép hiển thị các mục trong một danh sách hai cấp cuộn theo chiều dọc. Nó khác biệt với ListView thông thường ở chỗ nó hỗ trợ hai cấp độ: các nhóm mà người dùng có thể dễ dàng mở rộng và thu gọn để xem, và các mục con tương ứng bên trong mỗi nhóm.

Để tải dữ liệu vào các mục liên quan đến ExpandableListView, chúng ta sử dụng ExpandableListAdapter. Dưới đây là một số phương thức quan trọng mà lớp Adapter này thường sử dụng:

  • setChildIndicator(Drawable): Chúng ta dùng phương thức này để hiển thị một indicator bên cạnh mỗi mục con, thể hiện trạng thái hiện tại của nó. Nếu một mục con là mục cuối cùng trong một nhóm, trạng thái state_last sẽ được thiết lập.
  • setGroupIndicator(Drawable): Một indicator sẽ được vẽ bên cạnh mỗi nhóm để biểu thị trạng thái của nhóm đó – mở rộng (expanded) hoặc thu gọn (collapsed). Nếu nhóm rỗng, trạng thái state_empty được thiết lập. Nếu nhóm đang mở rộng, trạng thái state_expanded được thiết lập.
  • getGroupView(): Phương thức này trả về View cho tiêu đề nhóm (header của nhóm).
  • getChildView(): Phương thức này trả về View cho từng mục con riêng lẻ.

Chúng ta cũng sẽ làm việc với các interface đáng chú ý sau, được ExpandableListView sử dụng để xử lý các sự kiện tương tác:

  • ExpandableListView.OnChildClickListener: Chúng ta override interface này để triển khai phương thức callback được gọi khi một mục con trong danh sách được mở rộng và nhấp vào.
  • ExpandableListView.OnGroupClickListener: Chúng ta override interface này để triển khai phương thức callback được gọi khi tiêu đề của một nhóm trong danh sách được mở rộng và nhấp vào.
  • ExpandableListView.OnGroupCollapseListener: Chúng ta sử dụng interface này để nhận thông báo khi một nhóm được thu gọn.
  • ExpandableListView.OnGroupExpandListener: Chúng ta sử dụng interface này để nhận thông báo khi một nhóm được mở rộng.

Cấu trúc dự án của Android ExpandableListView

Dự án mẫu mà chúng ta sẽ xây dựng bao gồm ba lớp chính, mỗi lớp đóng một vai trò quan trọng trong việc triển khai ExpandableListView:

  • MainActivity: Lớp này chịu trách nhiệm hiển thị layout chính, chứa ExpandableListView và thiết lập các sự kiện tương tác.
  • ExpandableListDataPump: Lớp này đóng vai trò là nguồn dữ liệu. Chúng ta sẽ tạo một tập hợp dữ liệu ngẫu nhiên trong một List và ánh xạ các mục con vào các tiêu đề nhóm tương ứng bằng cách sử dụng HashMap.
  • CustomExpandableListAdapter: Lớp này là cầu nối giữa dữ liệu và ExpandableListView. Nó cung cấp cho MainActivity dữ liệu từ lớp ExpandableListDataPump để hiển thị lên giao diện.

Triển khai Code cho Android ExpandableListView

Layout activity_main.xml của chúng ta sẽ chỉ bao gồm một ExpandableListView nằm trong một RelativeLayout.

<RelativeLayout xmlns:android="<https://schemas.android.com/apk/res/android>"
    xmlns:tools="<https://schemas.android.com/tools>"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <ExpandableListView
        android:id="@+id/expandableListView"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:indicatorLeft="?android:attr/expandableListPreferredItemIndicatorLeft"
        android:divider="@android:color/darker_gray"
        android:dividerHeight="0.5dp" />

</RelativeLayout>

Thuộc tính android:indicatorLeft đặt giới hạn bên trái cho indicator của các mục. Một lưu ý quan trọng là chúng ta không thể sử dụng giá trị wrap_content cho thuộc tính android:layout_height của ExpandableListView trong XML trừ khi kích thước của parent đã được chỉ định rõ ràng.

Layout group định nghĩa giao diện cho tiêu đề của mỗi nhóm trong ExpandableListView. Chúng ta chỉ cần một TextView để hiển thị tên nhóm.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="<https://schemas.android.com/apk/res/android>"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/listTitle"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft"
        android:textColor="@android:color/black"
        android:paddingTop="10dp"
        android:paddingBottom="10dp" />
</LinearLayout>

Layout row định nghĩa giao diện cho từng mục con riêng lẻ trong mỗi nhóm. Tương tự, chúng ta cũng sử dụng một TextView để hiển thị nội dung của mục con.

<?xml version="10.0" encoding="utf-8"?>
<LinearLayout xmlns:android="<https://schemas.android.com/apk/res/android>"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:id="@+id/expandedListItem"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="?android:attr/expandableListPreferredChildPaddingLeft"
        android:paddingTop="10dp"
        android:paddingBottom="10dp" />
</LinearLayout>

Lớp ExpandableListDataPump chịu trách nhiệm tạo ra dữ liệu mẫu mà chúng ta sẽ hiển thị trên ExpandableListView. Chúng ta sử dụng một HashMap để ánh xạ các tiêu đề nhóm tới một List các mục con (List<String>).

package com.journaldev.expandablelistview;

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

public class ExpandableListDataPump {
    public static HashMap<String, List<String>> getData() {
        HashMap<String, List<String>> expandableListDetail = new HashMap<String, List<String>>();

        List<String> cricket = new ArrayList<String>();
        cricket.add("India");
        cricket.add("Pakistan");
        cricket.add("Australia");
        cricket.add("England");
        cricket.add("South Africa");

        List<String> football = new ArrayList<String>();
        football.add("Brazil");
        football.add("Spain");
        football.add("Germany");
        football.add("Netherlands");
        football.add("Italy");

        List<String> basketball = new ArrayList<String>();
        basketball.add("United States");
        basketball.add("Spain");
        basketball.add("Argentina");
        basketball.add("France");
        basketball.add("Russia");

        expandableListDetail.put("CRICKET TEAMS", cricket);
        expandableListDetail.put("FOOTBALL TEAMS", football);
        expandableListDetail.put("BASKETBALL TEAMS", basketball);
        return expandableListDetail;
    }
}

Trong đoạn code trên, đối tượng expandableListDetail chịu trách nhiệm ánh xạ các chuỗi tiêu đề nhóm tới các mục con tương ứng của chúng bằng cách sử dụng một ArrayList các chuỗi.

package com.journaldev.expandablelistview;

import java.util.HashMap;
import java.util.List;
import android.content.Context;
import android.graphics.Typeface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;

public class CustomExpandableListAdapter extends BaseExpandableListAdapter {

    private Context context;
    private List<String> expandableListTitle;
    private HashMap<String, List<String>> expandableListDetail;

    public CustomExpandableListAdapter(Context context, List<String> expandableListTitle,
                                       HashMap<String, List<String>> expandableListDetail) {
        this.context = context;
        this.expandableListTitle = expandableListTitle;
        this.expandableListDetail = expandableListDetail;
    }

    @Override
    public Object getChild(int listPosition, int expandedListPosition) {
        return this.expandableListDetail.get(this.expandableListTitle.get(listPosition))
                .get(expandedListPosition);
    }

    @Override
    public long getChildId(int listPosition, int expandedListPosition) {
        return expandedListPosition;
    }

    @Override
    public View getChildView(int listPosition, final int expandedListPosition,
                             boolean isLastChild, View convertView, ViewGroup parent) {
        final String expandedListText = (String) getChild(listPosition, expandedListPosition);
        if (convertView == null) {
            LayoutInflater layoutInflater = (LayoutInflater) this.context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = layoutInflater.inflate(R.layout.list_item, null);
        }
        TextView expandedListTextView = (TextView) convertView
                .findViewById(R.id.expandedListItem);
        expandedListTextView.setText(expandedListText);
        return convertView;
    }

    @Override
    public int getChildrenCount(int listPosition) {
        return this.expandableListDetail.get(this.expandableListTitle.get(listPosition))
                .size();
    }

    @Override
    public Object getGroup(int listPosition) {
        return this.expandableListTitle.get(listPosition);
    }

    @Override
    public int getGroupCount() {
        return this.expandableListTitle.size();
    }

    @Override
    public long getGroupId(int listPosition) {
        return listPosition;
    }

    @Override
    public View getGroupView(int listPosition, boolean isExpanded,
                             View convertView, ViewGroup parent) {
        String listTitle = (String) getGroup(listPosition);
        if (convertView == null) {
            LayoutInflater layoutInflater = (LayoutInflater) this.context.
                    getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = layoutInflater.inflate(R.layout.list_group, null);
        }
        TextView listTitleTextView = (TextView) convertView
                .findViewById(R.id.listTitle);
        listTitleTextView.setTypeface(null, Typeface.BOLD);
        listTitleTextView.setText(listTitle);
        return convertView;
    }

    @Override
    public boolean hasStableIds() {
        return false;
    }

    @Override
    public boolean isChildSelectable(int listPosition, int expandedListPosition) {
        return true;
    }
}

Lớp này extends BaseExpandableListAdapter và override các phương thức trong lớp cơ sở để cung cấp View cho ExpandableListView. Các phương thức như getGroupView()getChildView() có nhiệm vụ inflate layout tương ứng (list_group.xml hoặc list_item.xml) và điền dữ liệu vào View đó

Cuối cùng, lớp MainActivity sẽ tổng hợp tất cả các thành phần lại với nhau, khởi tạo ExpandableListView, thiết lập Adapter và xử lý các sự kiện click.

package com.journaldev.expandablelistview;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.Toast;

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

public class MainActivity extends AppCompatActivity {

    ExpandableListView expandableListView;
    ExpandableListAdapter expandableListAdapter;
    List<String> expandableListTitle;
    HashMap<String, List<String>> expandableListDetail;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        expandableListView = (ExpandableListView) findViewById(R.id.expandableListView);
        expandableListDetail = ExpandableListDataPump.getData();
        expandableListTitle = new ArrayList<String>(expandableListDetail.keySet());
        expandableListAdapter = new CustomExpandableListAdapter(this, expandableListTitle, expandableListDetail);
        expandableListView.setAdapter(expandableListAdapter);

        expandableListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
            @Override
            public void onGroupExpand(int groupPosition) {
                Toast.makeText(getApplicationContext(),
                        expandableListTitle.get(groupPosition) + " List Expanded.",
                        Toast.LENGTH_SHORT).show();
            }
        });

        expandableListView.setOnGroupCollapseListener(new ExpandableListView.OnGroupCollapseListener() {
            @Override
            public void onGroupCollapse(int groupPosition) {
                Toast.makeText(getApplicationContext(),
                        expandableListTitle.get(groupPosition) + " List Collapsed.",
                        Toast.LENGTH_SHORT).show();
            }
        });

        expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
            @Override
            public boolean onChildClick(ExpandableListView parent, View v,
                                        int groupPosition, int childPosition, long id) {
                Toast.makeText(
                        getApplicationContext(),
                        expandableListTitle.get(groupPosition)
                                + " -> "
                                + expandableListDetail.get(
                                expandableListTitle.get(groupPosition)).get(
                                childPosition), Toast.LENGTH_SHORT
                ).show();
                return false;
            }
        });
    }
}

Trong đoạn code trên, chúng ta đã triển khai tất cả các interface đã được thảo luận ở phần trước. Để đơn giản hóa, chúng ta chỉ hiển thị một Toast với tên của mục hoặc trạng thái của nhóm cho mỗi lần nhấp. Tuy nhiên, bạn có thể dễ dàng sửa đổi các phương thức callback này để thực hiện bất kỳ hoạt động nào khác mà ứng dụng của bạn yêu cầu, ví dụ như mở một Activity mới, cập nhật dữ liệu, hoặc tương tác với Fragment.

Lưu ý: ExpandableListView có khả năng cuộn theo mặc định. Điều này kết thúc phần hướng dẫn chi tiết về code ExpandableListView trong Android.

Kết luận

Qua bài blog này, chúng ta đã cùng nhau khám phá ExpandableListView – một View mạnh mẽ giúp chúng ta tổ chức và hiển thị dữ liệu phân cấp một cách trực quan và dễ sử dụng trong các ứng dụng Android. Chúng ta đã tìm hiểu cách nó hoạt động dựa trên cấu trúc groupchildren, cùng với vai trò trung tâm của ExpandableListAdapter trong việc cung cấp dữ liệu. Hãy mạnh dạn áp dụng ExpandableListView vào các dự án của bạn để nâng cao trải nghiệm người dùng, giúp họ tiếp cận thông tin một cách nhanh chóng và hiệu quả nhất.

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.

Đăng ký nhận Newsletter

Nhận các nội dung hữu ích mới nhất