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ộtindicator
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áistate_last
sẽ được thiết lập.setGroupIndicator(Drawable)
: Mộtindicator
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áistate_empty
được thiết lập. Nếu nhóm đang mở rộng, trạng tháistate_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ứaExpandableListView
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ộtList
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ụngHashMap
.CustomExpandableListAdapter
: Lớp này là cầu nối giữa dữ liệu vàExpandableListView
. Nó cung cấp choMainActivity
dữ liệu từ lớpExpandableListDataPump
để 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()
và 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 group
và children
, 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.