現役プログラマのプログラミング教室@岡崎市康生, Android/iOS(Swift/Kotlin)のアプリ開発、デジタル漫画制作を主軸事業としています。

Android(Kotlin)のExpandableListViewでindicatorを右に表示してサイズ変更、CheckBoxやRadioButtonをカスタム画像で表示するサンプル

恥ずかしながら案件を受けるまでやったことがなく、意外と検索してもこれというサンプルが見つからなかったので、備忘録として残します。

※すみません、ベースはどなたかの車のサンプルコード参考に改修しました。

ポイントは

  • Kotlin
  • ExpandableListViewのカスタマイズ
  • 矢印などのインジケーターをカスタムかつ右に配置
  • インジケータのサイズ調整
  • チェックボックスやラジオボタンをカスタム表示

百聞は一見にしかずということでソースコードはこちら。

テスト作成なので、冗長なコードは修正くださいm ( _ _ ) m

ソースコード

リストを表示するFragment

ExpandableListViewの用意したデータをもとにアダプター作成してセットするだけ

package com.pgworkslabo.testexpandablelistview

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ExpandableListView
import android.widget.Toast
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.fragment_first.*


/**
 * A simple [Fragment] subclass as the default destination in the navigation.
 */
class FirstFragment : Fragment() {

    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_first, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        //ExpandableListViewの初期化
        val exListView: ExpandableListView = exlistview
        activity?.applicationContext?.also {
            val adapter = CarMakerListAdapter(it, ConditionData.data.first, ConditionData.data.second)
            exListView.setAdapter(adapter)
            exListView.setOnChildClickListener { parent, _, groupPosition, childPosition, _ -> //子要素をタップした時の処理
                //このサンプルではToastメッセージを表示するだけ
                (parent.expandableListAdapter as CarMakerListAdapter).also { adapt ->
                    val mName = adapt.getGroup(groupPosition) as String //親要素からメーカー名を取得
                    val cName = adapt.getChild(groupPosition, childPosition) as String //子要素から車名を取得
                    Toast.makeText(
                        it,
                        "$mName : $cName",
                        Toast.LENGTH_LONG
                    ).show() //Toast生成
                }

                true
            }

            // この方法でもインジケーターが反転する
            //            val vto: ViewTreeObserver = exListView.viewTreeObserver
            //
            //            vto.addOnGlobalLayoutListener {
            //                exListView.setIndicatorBounds(exListView.right -160, exListView.width)
            //            }
        }
    }
}

BaseExpandableListAdapterを継承したアダプタ

iOSのSectionにあたる親とRowにあたる子の描画をカスタマイズ

package com.pgworkslabo.testexpandablelistview

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseExpandableListAdapter
import kotlinx.android.synthetic.main.listitem_cars.view.*
import kotlinx.android.synthetic.main.listitem_makers.view.*


internal class CarMakerListAdapter //コンストラクタ
    (
    var context: Context,
    //メンバ変数
    var listMaker //親要素のリスト
    : List<String>,
    //子要素のリスト
    var listCar: List<List<String>>
) :
    BaseExpandableListAdapter() {

    override fun getGroupCount(): Int {
        return listMaker.size //親要素の数を返す
    }

    override fun getChildrenCount(groupPosition: Int): Int {
        return listCar[groupPosition].size //子要素の数を返す
    }

    override fun getGroup(groupPosition: Int): Any {
        return listMaker[groupPosition] //親要素を取得
    }

    override fun getChild(groupPosition: Int, childPosition: Int): Any {
        return listCar[groupPosition][childPosition] //子要素を取得
    }

    override fun getGroupId(groupPosition: Int): Long {
        //親要素の固有IDを返す
        //このサンプルでは固有IDは無いので0
        return 0
    }

    override fun getChildId(groupPosition: Int, childPosition: Int): Long {
        //子要素の固有IDを返す
        //このサンプルでは固有IDは無いので0
        return 0
    }

    override fun hasStableIds(): Boolean {
        //固有IDを持つかどうかを返す
        //このサンプルでは持たないのでfalse
        return false
    }

    override fun getGroupView(
        groupPosition: Int,
        isExpanded: Boolean,
        convertView: View?,
        parent: ViewGroup
    ): View? {

        var cv = if (convertView == null) {
            val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            inflater.inflate(R.layout.listitem_makers, parent, false)
        } else {
            convertView
        }
        cv?.item_makers_name.also {
            it?.text = listMaker[groupPosition]
        }
        return cv
    }

    override fun getChildView(
        groupPosition: Int,
        childPosition: Int,
        isLastChild: Boolean,
        convertView: View?,
        parent: ViewGroup
    ): View? {

        //子要素のレイアウト生成
        var cv = convertView
        if (cv == null) {
            val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            cv = inflater.inflate(R.layout.listitem_cars, parent, false)
        }

        cv?.item_cars_name.also {
            it?.text = listCar[groupPosition][childPosition]
        }

        when(listMaker[groupPosition]){
            "TOYOTA"->{
                cv?.condition_check_box?.visibility = View.VISIBLE
                cv?.condition_radio_button?.visibility = View.GONE
            }
            else->{
                cv?.condition_check_box?.visibility = View.GONE
                cv?.condition_radio_button?.visibility = View.VISIBLE
            }
        }

        return cv
    }

    override fun isChildSelectable(groupPosition: Int, childPosition: Int): Boolean {
        //子要素がタップ可能かどうかを返す
        //このサンプルでは可能にするのでtrue
        return true
    }

}

データ作成

セクションとローのデータ

package com.pgworkslabo.testexpandablelistview
import java.util.*
internal object ConditionData {
    val data: Pair<List<String>, List<List<String>>>
        get() {
            //親要素のリスト
            val makers: MutableList<String> = ArrayList()
            makers.add("TOYOTA")
            makers.add("MAZDA")
            makers.add("HONDA")

            //子要素のリスト
            val carsToyota: MutableList<String> = ArrayList()
            carsToyota.add("CROWN")
            carsToyota.add("PRIUS")
            carsToyota.add("COROLLA")
            carsToyota.add("VITZ")
            val carsMazda: MutableList<String> = ArrayList()
            carsMazda.add("ATENZA")
            carsMazda.add("AXELA")
            carsMazda.add("DEMIO")
            val carsHonda: MutableList<String> = ArrayList()
            carsHonda.add("LEGEND")
            carsHonda.add("CIVIC")
            carsHonda.add("FIT")

            val cars: MutableList<List<String>> =
                ArrayList()
            cars.add(carsToyota)
            cars.add(carsMazda)
            cars.add(carsHonda)

            return Pair(makers, cars)
        }
}

レイアウト

AndroidJavaやってた人にはお決まりですかね

Fragment表示

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
    tools:context=".FirstFragment">

    <ExpandableListView
        android:background="@android:color/white"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:indicatorLeft="10dp"
        android:indicatorRight="60dp"
        android:groupIndicator="@drawable/condition_indicator"
        android:layoutDirection="rtl"
        android:id="@+id/exlistview"/>
</androidx.constraintlayout.widget.ConstraintLayout>

セクション部分のメーカーのレイアウト

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    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="wrap_content">

        <TextView
            android:id="@+id/item_makers_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="test"
            android:padding="16sp"
            android:layoutDirection="ltr"
            android:textColor="@color/cardview_dark_background"
            android:fontFamily="sans-serif-medium"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

メーカーの子にあたる車のレイアウト

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    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="wrap_content">

        <TextView
            android:id="@+id/item_cars_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="test"
            android:padding="16sp"
            android:layout_marginLeft="12dp"
            android:layoutDirection="ltr"
            android:textColor="@color/cardview_dark_background"
            android:fontFamily="sans-serif-medium"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            />

        <CheckBox
            android:visibility="visible"
            android:id="@+id/condition_check_box"
            style="@style/ConditionCheckBox"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="20dp"
            android:layout_gravity="right|center_vertical"
            />

        <RadioButton
            android:visibility="visible"
            android:id="@+id/condition_radio_button"
            style="@style/ConditionRadioButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="20dp"
            android:layout_gravity="right|center_vertical"
            />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

表示画像をカスタムするためのcheckboxのdrawable

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:state_checked="true"
        android:drawable="@drawable/checkboxon" />
    <item
        android:state_checked="false"
        android:drawable="@drawable/checkboxoff" />
</selector>

表示画像をカスタムするためのradio buttonのdrawable

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:state_checked="true"
        android:drawable="@drawable/downloadon" />
    <item
        android:state_checked="false"
        android:drawable="@drawable/downloadoff" />
</selector>

indicatorを右に配置、アイコンサイズを調整するdrawable

結局left,right,top,bottomをpaddingのように利用。

<?xml version="1.0" encoding="utf-8"?>

<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:state_empty="true">
        <layer-list>
            <item
                android:left="10dp"
                android:right="10dp"
                android:top="10dp"
                android:bottom="10dp"
                android:drawable="@drawable/list_close">
            </item>
        </layer-list>
    </item>

    <item android:state_expanded="true">
        <layer-list>
            <item
                android:left="10dp"
                android:right="10dp"
                android:top="10dp"
                android:bottom="10dp"
                android:drawable="@drawable/list_open">
            </item>
        </layer-list>
    </item>

    <item>
        <layer-list>
            <item
                android:left="2dp"
                android:right="2dp"
                android:top="10dp"
                android:bottom="10dp"
                android:drawable="@drawable/list_close">
            </item>
        </layer-list>
    </item>
</selector>

チェックボックスとラジオボタンのstyle

<resources>
    <style name="ConditionCheckBox" parent="android:Widget.CompoundButton.CheckBox">
        <item name="android:button">@drawable/condition_checkbox</item>
    </style>

    <style name="ConditionRadioButton" parent="android:Widget.CompoundButton.RadioButton">
        <item name="android:button">@drawable/condition_radiobutton</item>
    </style>
</resources>