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

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

この記事からわかること

  • Android Studio/KotlinExpandableListView使い方
  • 折り畳み(アコーディオン)を実装する方法

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

折り畳み(アコーディオン)ビュー

Androidアプリで以下のようにクリックすることで要素が表示/非表示されるような折り畳み(アコーディオン)ビューを実装する方法をまとめていきます。

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

実装方法は色々あるかと思いますがExpandableListViewを使用すると簡単に実装することができました。

ExpandableListViewとは?

ExpandableListView折りたたみ可能なリストを表示するために使用されるUIコンポーネントです。内部的に親要素(グループ)と子要素(アイテム)を保持し、親項目がクリックされたときに子要素を表示させることができます。

仕組みはRecyclerViewなどと同じでAdapterを介してViewとデータのやり取りを行います。

実装手順

  1. ExpandableListViewを設置した画面を用意
  2. 親要素表示用のViewをレイアウト
  3. 子要素表示用のViewをレイアウト
  4. Adapterクラスを実装
  5. 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"
【Kotlin/Android】ExpandableListViewの使い方!折り畳み(アコーディオン)ビュー

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をセット

最後にExpandableListViewAdapterをセットすれば完了です。


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を更新したい時があると思います。

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

その際は以下のように実装します。

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
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index