【Kotlin/Android】ExpandableListViewの使い方!折り畳み(アコーディオン)ビュー

この記事からわかること
- Android Studio/KotlinのExpandableListViewの使い方
- 折り畳み(アコーディオン)を実装する方法
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Flamingo
- Kotlin:1.8.20
折り畳み(アコーディオン)ビュー
Androidアプリで以下のようにクリックすることで要素が表示/非表示されるような折り畳み(アコーディオン)ビューを実装する方法をまとめていきます。

実装方法は色々あるかと思いますがExpandableListView
を使用すると簡単に実装することができました。
ExpandableListViewとは?
ExpandableListView
は折りたたみ可能なリストを表示するために使用されるUIコンポーネントです。内部的に親要素(グループ)と子要素(アイテム)を保持し、親項目がクリックされたときに子要素を表示させることができます。
仕組みはRecyclerView
などと同じでAdapter
を介してViewとデータのやり取りを行います。
実装手順
- ExpandableListViewを設置した画面を用意
- 親要素表示用のViewをレイアウト
- 子要素表示用のViewをレイアウト
- Adapterクラスを実装
- 5.ExpandableListViewにAdapterをセット
1.ExpandableListViewを設置した画面を用意
まずはFragmentなどのレイアウトファイルにExpandableListView
を追加します。
<ExpandableListView
android:id="@+id/expandable_list_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:groupIndicator="@null"
android:layout_marginVertical="20dp"
android:layout_marginHorizontal="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/include_header"/>
デフォルトのUIでは左端に三角のアイコンが表示されるので不要であれば以下を指定することでアイコンを非表示にすることができます。
android:groupIndicator="@null"

2.親要素表示用のViewをレイアウト
続いて親要素を表示するViewのレイアウトファイルを追加します。ここのレイアウトは自由です。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/white"
android:paddingVertical="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="20dp"
android:background="@color/white">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/icon_question"
app:tint="@color/ex_thema" />
<TextView
android:id="@+id/question_title_label"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:paddingHorizontal="10dp"
android:text="@string/how_to_use_q1"
android:textColor="@color/ex_text"
android:textSize="14sp"
android:textStyle="bold"/>
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center" />
<ImageView
android:id="@+id/icon_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/icon_plus" />
</LinearLayout>
</LinearLayout>
3.子要素表示用のViewをレイアウト
同じく子要素を表示するViewのレイアウトファイルを追加します。ここのレイアウトも自由です。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/white"
android:paddingVertical="10dp">
<TextView
android:id="@+id/answer_title_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:paddingHorizontal="10dp"
android:text="AnswerAnswerAnswerAnswerAnswerAnswer"
android:textColor="@color/ex_text"
android:textSize="14sp" />
</LinearLayout>
4.Adapterクラスを実装
続いてアコーディオンビューとデータを紐付けるためのAdapterクラスを実装します。BaseExpandableListAdapter
を継承さえて中身を実装していきます。
class MyExpandableListAdapter (
private val context: Context,
): BaseExpandableListAdapter() {
// データリスト(今回は中に定義)
// Map<String, List<String>>などの形式で用意すればOK
// 今回はリソースから取得し1つの親に対して1つの子のみ
private val dataList: Map<String, List<String>> =
mapOf(
context.getString(R.string.how_to_use_q1) to listOf(context.getString(R.string.how_to_use_a1)),
context.getString(R.string.how_to_use_q2) to listOf(context.getString(R.string.how_to_use_a2)),
context.getString(R.string.how_to_use_q3) to listOf(context.getString(R.string.how_to_use_a3)),
context.getString(R.string.how_to_use_q4) to listOf(context.getString(R.string.how_to_use_a4))
)
// 親要素の数
override fun getGroupCount(): Int {
if (!dataList.isNullOrEmpty()) {
return dataList.keys.size
}
return 0
}
// 子要素の数
override fun getChildrenCount(p0: Int): Int {
if (!dataList.isNullOrEmpty()) {
val key = dataList.keys.elementAt(p0)
val list = dataList[key]
if (!list.isNullOrEmpty()) {
return list.size
}
}
return 0
}
// 親要素を取得
override fun getGroup(p0: Int): Any {
if (!dataList.isNullOrEmpty()) {
return dataList.keys.elementAt(p0)
}
return ""
}
// 子要素を取得
override fun getChild(p0: Int, p1: Int): Any {
if (!dataList.isNullOrEmpty()) {
val key = dataList.keys.elementAt(p0)
val list = dataList[key]
if (!list.isNullOrEmpty()) {
return list[p1]
}
}
return ""
}
// 親要素の固有ID
override fun getGroupId(p0: Int): Long {
return 0
}
// 子要素の固有ID
override fun getChildId(p0: Int, p1: Int): Long {
return 0
}
// 固有IDを持つかどうか
override fun hasStableIds(): Boolean {
return false
}
// 親要素のViewを構築
override fun getGroupView(p0: Int, p1: Boolean, p2: View?, p3: ViewGroup?): View {
val title = if (!dataList.isNullOrEmpty()) dataList.keys.elementAt(p0) else ""
var convertView = p2
if (convertView == null) {
val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
convertView = layoutInflater.inflate(R.layout.component_parent_row, null)
}
val titleTextView = convertView!!.findViewById<TextView>(R.id.question_title_label)
titleTextView.text = title
return convertView
}
// 子要素のViewを構築
override fun getChildView(p0: Int, p1: Int, p2: Boolean, p3: View?, p4: ViewGroup?): View {
val key = dataList.keys.elementAt(p0)
val list = dataList[key]
val title = if (!list.isNullOrEmpty()) list[p1] else ""
var convertView = p3
if (convertView == null) {
val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
convertView = layoutInflater.inflate(R.layout.component_child_row, null)
}
val titleTextView = convertView!!.findViewById<TextView>(R.id.answer_title_label)
titleTextView.text = title
return convertView
}
// 子要素がタップ可能かどうか
override fun isChildSelectable(p0: Int, p1: Int): Boolean {
return true
}
}
5.ExpandableListViewにAdapterをセット
最後にExpandableListView
にAdapter
をセットすれば完了です。
var expandableList: ExpandableListView = view.findViewById(R.id.expandable_list_view)
var adapter = MyExpandableListAdapter(this.requireContext())
expandableList.setAdapter(adapter)
初期表示に折り畳まないようにする
デフォルトでは子要素は折り畳まれた状態ですが、初期から折り畳まないようにしたり、コードから表示させるにはexpandGroup
メソッドを使用します。
expandableList.expandGroup(0)
親要素(グループ)にタップイベントを追加する
親要素(グループ)にタップイベントを追加するにはsetOnGroupClickListener
を使用します。コールバックの返り値にはBoolean
型を指定します。true
にすると記述した処理だけを行い子要素を展開しません。
expandableList.setOnGroupClickListener { parent, v, groupPosition, id ->
true
}
子要素(アイテム)にタップイベントを追加する
子要素(アイテム)にタップイベントを追加するにはsetOnGroupClickListener
を使用します。コールバックの返り値にはBoolean
型を指定します。true
にするとクリックイベントが消費されたと見なされ、他の処理は行われません。
expandableList.setOnChildClickListener { parent, v, groupPosition, childPosition, id ->
false
}
親要素をタップした時にViewを変更する
以下動画のように展開された時に親要素のアイコンを「ー
」に変更したりなど親のViewを更新したい時があると思います。

その際は以下のように実装します。
1.アコーディオン用のデータモデルを作成
展開しているかどうかの状態を保持するデータモデルを作成します。
data class HowToUseQaData(
val question: String, // 親要素の値
val answer: String, // 子要素の値
var expanded: Boolean // 展開しているかどうか
)
2.Adapterに状態変更メソッドを実装
Adapterクラスにデータモデルの展開状態を切り替えるメソッドを用意します。
class MyExpandableListAdapter (
private val context: Context,
): BaseExpandableListAdapter() {
// データモデルのリストに変更
private val dataList: List<HowToUseQaData> =
listOf(
HowToUseQaData(context.getString(R.string.how_to_use_q1), context.getString(R.string.how_to_use_a1), false),
HowToUseQaData(context.getString(R.string.how_to_use_q2), context.getString(R.string.how_to_use_a2), false),
HowToUseQaData(context.getString(R.string.how_to_use_q3), context.getString(R.string.how_to_use_a3), false),
HowToUseQaData(context.getString(R.string.how_to_use_q4), context.getString(R.string.how_to_use_a4), false),
)
// 〜〜〜〜〜〜〜〜〜〜〜
// 親要素のViewを構築
override fun getGroupView(p0: Int, p1: Boolean, p2: View?, p3: ViewGroup?): View {
val title = if (!dataList.isNullOrEmpty()) dataList.elementAt(p0).question else ""
var convertView = p2
if (convertView == null) {
val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
convertView = layoutInflater.inflate(R.layout.component_how_to_question_row, null)
}
val titleTextView: TextView = convertView!!.findViewById(R.id.question_title_label)
val accordionIcon: ImageView = convertView!!.findViewById(R.id.accordion_icon)
if (dataList.elementAt(p0).expanded) {
var icon: Drawable? = ResourcesCompat.getDrawable(context.resources, R.drawable.icon_minus, null)
accordionIcon.setImageDrawable(icon)
} else {
var icon: Drawable? = ResourcesCompat.getDrawable(context.resources, R.drawable.icon_plus, null)
accordionIcon.setImageDrawable(icon)
}
titleTextView.text = title
return convertView
}
// 〜〜〜〜〜〜〜〜〜〜〜
/** アコーディオンアイコンを切り替える */
public fun toggleAccordionIcon(index: Int) {
dataList[index].expanded = !dataList[index].expanded
notifyDataSetChanged()
}
}
3.Fragmentからタップイベントを登録
最後にFragmentからsetOnGroupClickListener
を利用して先ほどの切り替えメソッドを呼び出せば完了です。
expandableList.setOnGroupClickListener { parent, v, groupPosition, id ->
adapter.toggleAccordionIcon(groupPosition)
false
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。