Android-雙層清單ExpandableListView

Vincent Zheng
新手工程師的程式教室
11 min readApr 24, 2019

對初學者來說,應該都曾經學過ListView,它是一個能用來顯示清單的元件。不過ListView是「單層式」的,只能簡單地顯示一條一條的項目。

本文要介紹的ExpandableListView是「雙層式」,外觀像ListView,但是每個群組項目可以繼續點擊展開或收合,裡頭包含了多個子項目。實作方式也會稍微繁瑣一些。本文使用Kotlin語言。

一、添加Layout元件

首先在Activity的XML檔加入ExpandableListView這個元件

<ExpandableListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="#800000FF"
android:childDivider="#80FF0000"
android:dividerHeight="1dp"/>

下面介紹一些實用的屬性

divider、childDevider:分別為群組項目與子項目的分隔線顏色。

groupIndicator:群組項目前面的icon,預設為根據展開或收合來顯示上下箭頭。若不想要這個icon,可設值為「@null」。

childIndicator:子項目前面的icon,預設無圖案。

dividerHeight:分隔線的粗細,不分群組項目或子項目,是統一設定的。

除此之外,群組項目和子項目也需要對應的Layout,這裡先定義如下,下面會再解說本文範例的情境。

群組項目的版面配置(item_department.xml)

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/txtDepartmentName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_marginStart="40dp"
android:textSize="20sp"
android:textColor="#0000FF"/>

</LinearLayout>

子項目的版面配置(item_class.xml)

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/txtClassName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_marginStart="40dp"
android:textSize="17sp"
android:textColor="#FF0000"/>

</LinearLayout>

二、建立Adapter

ListView需要Adapter來串接資料和項目畫面,ExpandableListView也是如此。但與ListView不同的是,它使用「BaseExpandableListAdapter」,這樣才能分別串接群組項目及子項目。

class ExpandableListViewAdapter()
: BaseExpandableListAdapter() {

}

繼承BaseExpandableListAdapter後,需要實作10個方法。我們在下一節再實作它們,這一節先說明要顯示的測試資料,並完成Adapter的建構式。

實作ListView的BaseAdapter時,一個簡單的例子就是透過建構式傳入一個List。但ExpandableListView是雙層的,因此筆者在本文的範例中需要同時傳入群組項目和子項目的List。

本文的範例是:群組項目為「科系名稱」,子項目為「班級名稱」,因此在建構式加入兩個List的參數。

class ExpandableListViewAdapter(
private val context: Context,
private val departments: List<String>,
private val classes: List<List<String>>)
: BaseExpandableListAdapter() {
}

其中班級名稱使用「List<List<String>>」的理由是,每個科系中會有多個班級。為了儲存所有科系的班級名稱,需要在一個List中存放各個群組的班級List。而傳入Context是為了之後載入Layout檔使用。

三、實作Adapter

在上一節繼承BaseExpandableListAdapter後,會發現有10個方法要實作,本節就來一一完成,其實大部份都跟ListView的BaseAdapter有對應關係。

1.取得群組物件和子項目物件

override fun getGroup(groupPosition: Int): Any {
return departments[groupPosition]
}
override fun getChild(groupPosition: Int, childPosition: Int): Any {
return classes[groupPosition][childPosition]
}

2.取得群組數量和其內部子項目的數量

override fun getGroupCount(): Int {
return departments.size
}
override fun getChildrenCount(groupPosition: Int): Int {
return classes[groupPosition].size
}

3.項目ID的相關設定,依照需求決定回傳值即可,適當運用將對效能有幫助

override fun getGroupId(groupPosition: Int): Long {
return groupPosition.toLong()
}
override fun getChildId(groupPosition: Int, childPosition: Int): Long {
return (groupPosition * 100 + childPosition).toLong()
}
override fun hasStableIds(): Boolean {
return true
}

4.定義子項目是否可以被點擊。設為true才能進而觸發點擊事件,設為false的話會讓分隔線一併消失。

override fun isChildSelectable(groupPosition: Int, childPosition: Int): Boolean {
return true
}

5.串接資料與項目畫面

override fun getGroupView(groupPosition: Int, isExpanded: Boolean, convertView: View?, parent: ViewGroup?): View {
val view = convertView ?: LayoutInflater.from(context).inflate(R.layout.item_department, null)
val textView = view.findViewById<TextView>(R.id.txtDepartmentName)
textView.text = departments[groupPosition]

return view
}
override fun getChildView(groupPosition: Int, childPosition: Int, isLastChild: Boolean, convertView: View?, parent: ViewGroup?): View {
val view = convertView ?: LayoutInflater.from(context).inflate(R.layout.item_class, null)
val textView = view.findViewById<TextView>(R.id.txtClassName)
textView.text = classes[groupPosition][childPosition]

return view
}

四、觀看結果

在Activity準備好測試資料,建立Adapter,並指派給ExpandableListView就完成了。

val listView = findViewById<ExpandableListView>(R.id.listView)

val departments = listOf("資訊管理系", "財務金融系", "會計系")

val classes = listOf(
listOf("一年甲班", "二年甲班", "三年甲班", "四年甲班"),
listOf("一年甲班", "二年甲班", "三年甲班", "四年甲班"),
listOf("一年甲班", "一年乙班", "二年甲班", "二年乙班",
"三年甲班", "三年乙班", "四年甲班", "四年乙班")
)

val adapter = ExpandableListViewAdapter(this, departments, classes)
listView.setAdapter(adapter)
雙層式清單

五、點擊事件

點擊群組項目。若回傳值為true,則不會展開清單。

listView.setOnGroupClickListener { parent, v, groupPosition, id ->
val departmentName = adapter.getGroup(groupPosition).toString()
Toast.makeText(this@MainActivity, departmentName, Toast.LENGTH_SHORT).show()

false
}
點擊群組項目

點擊子項目

listView.setOnChildClickListener { parent, v, groupPosition, childPosition, id ->
val departmentName = adapter.getGroup(groupPosition).toString()
val className = adapter.getChild(groupPosition, childPosition).toString()
val classTitle = "$departmentName$className"
Toast.makeText(this@MainActivity, classTitle, Toast.LENGTH_SHORT).show()

false
}
點擊子項目

--

--

Vincent Zheng
新手工程師的程式教室

我是Vincent,是個來自資管系的後端軟體工程師。當初因為學校作業,才踏出寫部落格的第一步。這裡提供程式教學文章,包含自學和工作上用到的經驗,希望能讓讀者學到東西。我的部落已搬家至 https://chikuwa-tech-study.blogspot.com/