[Kotlin] ListViewの項目にボタン付きの自作レイアウトを設定

2018年1月18日(更新: 2018年4月26日)

前回は ListViewのリスト項目をタップした際のイベント処理 について紹介しました。この記事では、リスト項目そのものをタップした際に処理を行うプログラムについて書きました。

今回は、リスト項目としてテキスト以外にボタン(ImageButton)を追加したレイアウトを設定し、そのボタンをクリックした時に処理を行えるような ListView の作り方を紹介したいと思います。

紹介するサンプルを完成させると、以下のように削除ボタンを持つリストが表示されたAndroidアプリができます。

ボタン付きのリストアイテムを持つListView

サンプルアプリのレイアウトとプログラム

アプリ全体のレイアウトとなる activity_main.xml の中身は以前紹介したものと同じです。

これに加え、今回は自作のXMLレイアウトをリスト項目のデザインとして使用しますので、ディレクトリ res/layout に新しいXMLファイルを作成します。

この例では、名前を list_item.xml とし、中身を以下のようにします。

list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:descendantFocusability="blocksDescendants">

    <TextView
        android:id="@+id/item_title"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_toLeftOf="@+id/delete_button"
        android:layout_toStartOf="@+id/delete_button"
        android:padding="8dp" />

    <TextView
        android:id="@+id/item_description"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:padding="8dp" />

    <ImageButton
        android:id="@+id/delete_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:padding="16dp"
        android:background="#00000000"
        android:src="@drawable/ic_delete_forever_black_48dp" />

</RelativeLayout>

タイトルと説明を表示するための TextView と、ボタンとなる画像を表示する ImageButton を設定しています。ボタンに使用する画像は res/drawable の中に保存して下さい。

次に Kotlin のプログラム(MainActivity.kt)を以下のように書き換えます。

MainActivity.kt

package com.example.test

import android.content.Context
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageButton
import android.widget.ListView
import android.widget.TextView

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 初期のリスト項目を設定
        val arrayAdapter = MyArrayAdapter(this, 0).apply {
            add(ListItem("Android", "Google"))
            add(ListItem("iOS", "Apple"))
            add(ListItem("Windows"))
            add(ListItem("macOS"))
            add(ListItem("Unix"))
        }

        // ListView にリスト項目と ArrayAdapter を設定
        val listView : ListView = findViewById(R.id.listView)
        listView.adapter = arrayAdapter
    }
}

// リスト項目のデータ
class ListItem(val title : String) {

    var description : String = "No description"

    constructor(title: String, description: String) : this(title) {
        this.description = description
    }
}

// リスト項目を再利用するためのホルダー
data class ViewHolder(val titleView: TextView, val descriptionView: TextView, val deleteIcon: ImageButton)

// 自作のリスト項目データを扱えるようにした ArrayAdapter
class MyArrayAdapter : ArrayAdapter<ListItem> {

    private var inflater : LayoutInflater? = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?

    constructor(context : Context, resource : Int) : super(context, resource) {}

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {

        var viewHolder : ViewHolder? = null
        var view = convertView

        // 再利用の設定
        if (view == null) {

            view = inflater!!.inflate(R.layout.list_item, parent, false)

            viewHolder = ViewHolder(
                    view.findViewById(R.id.item_title),
                    view.findViewById(R.id.item_description),
                    view.findViewById(R.id.delete_button)
            )
            view.tag = viewHolder
        } else {
            viewHolder = view.tag as ViewHolder
        }

        // 項目の情報を設定
        val listItem = getItem(position)
        viewHolder.titleView.text = listItem!!.title
        viewHolder.descriptionView.text = listItem.description
        viewHolder.deleteIcon.setOnClickListener { _ ->
            // 削除ボタンをタップしたときの処理
            this.remove(listItem)
            this.notifyDataSetChanged()
        }

        return view!!
    }
}

新しく作成したクラスが複数ありますので、順番に説明します。

ListItem

今回は、リスト項目に表示する情報として以下の2つが設定できるようになっています。

  • タイトル(title)
  • 説明文(description)

タイトルは必須項目で、説明文はデフォルト値が用意されている任意項目とします。これを表現したクラスが以下の ListItem です。

// リスト項目のデータ
class ListItem(val title : String) {

    var description : String = "No description"

    constructor(title: String, description: String) : this(title) {
        this.description = description
    }
}

Kotlin では、クラス宣言と同じ行にコンストラクタとプロパティ変数の定義を同時に書くことができます。任意のコンストラクタ(セカンダリコンストラクタ)に説明文を設定するための第二引数を設定できるようにしています。

このクラスからリストの項目を作成するので MyArrayAdapter(後述) に以下のように追加していきます。

...

val arrayAdapter = MyArrayAdapter(this, 0).apply {
    add(ListItem("Android", "Google")) // 第二引数で説明文を設定した ListItem
    add(ListItem("iOS", "Apple"))
    add(ListItem("Windows")) // 第二引数を省略した ListItem
    add(ListItem("macOS"))
    add(ListItem("Unix"))
}

...

MyArrayAdapter

上記の自作クラス ListItem をリスト項目として使えるように ArrayAdapter を拡張したクラスです。

オーバーライドしたメソッド getView の中で、リスト項目の表示内容の設定とリスト項目を再利用するための設定を行っています。

再利用とは、項目が増えてスクロールが必要となった際に、項目の作り直しを最小限にするためにデータを使いまわすことです。そのためのデータ保持を行うクラスが ViewHolder です。

これによって何度も findViewById が呼び出されることを防ぎ、アプリの動作が遅くなるのを防ぎます。詳細は以下のページをご覧ください。

Making ListView Scrolling Smooth | Android Developers

以上で、ボタン付きの自作レイアウトを設定した項目を表示する ListView ができます。削除ボタンを押すと、実際にリスト項目が削除されるのが確認できます。

コメントを残す

メールアドレスが公開されることはありません。