プログラマになる

組み込みエンジニアで主にC言語、時々C#をやっている人の技術メモです。最近はAndroidも少しずつ勉強中

今更ながらListViewをまとめる - ViewHolder編 -

はじめに

今更ながらListViewをまとめるの続きです。
今回はViewHolderについて説明してみたいと思います。
またPersonクラスを生成してそのプロパティをListViewとViewHolderを使って表示してみます。

1. ListViewに表示するItemを作成

ここは前回と同じくPersonクラスを定義します。

Person.kt

class Person(firstName : String, lastName : String, age : Int) {
    val firstName : String = firstName
    val lastName : String = lastName
    val age : Int = age
}

またPersonクラスを表示するためのLayoutを作成します。
今回も単純にPersonクラスを表示するものしています。

list_item.xml

f:id:kaleidot725:20180624162651p:plain

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:id="@+id/personItem"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:id="@+id/firstName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="firstName"
        android:background="#FF9999"/>

    <TextView
        android:id="@+id/lastName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="lastName"
        android:background="#99FF99"/>

    <TextView
        android:id="@+id/age"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="age"
        android:background="#9999FF"/>
</LinearLayout>

2.ViewHolderを使ったAdapterを作成する

list_item.xmlをViewとして生成するためにAdapterを作成します。
また今回はViewHolderを実装して、ListViewの表示速度を改善したいと思います。

なぜViewHolderが必要なのか?

まずViewHolderとはなんぞやという感じだと思いますが、
その名の通りViewを保持する役割のクラスです。
じゃあなぜViewHolderでViewを保持する必要があるのでしょうか?
ViewHolderを使わない場合のコードを確認してみましょう。

ListViewAdapter

class ListViewAdapter(fragmentActivity: FragmentActivity?, resource : Int, list: List<Person>) : ArrayAdapter<Person>(fragmentActivity, resource, list) {
    private val inflater : LayoutInflater = LayoutInflater.from(context)

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

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

        val firstNameTextView = view.findViewById<TextView>(R.id.firstName)
        firstNameTextView.text = super.getItem(position).firstName

        val lastNameTextView = view.findViewById<TextView>(R.id.lastName)
        lastNameTextView.text = super.getItem(position).lastName

        val ageTextView = view.findViewById<TextView>(R.id.age)
        ageTextView.text = super.getItem(position).age.toString()

        return view
    }
}

getViewが呼ばれるたびにViewを生成してfindViewByIdを使って値をセットしています。
このfindViewByIdがパフォーマンス上問題となります。
findViewByIdは呼び出しコストが高く、回数が多くなるつまりViewが増えるほどパフォーマンスが低下します。
つまりListViewを高速化したい場合はどうにかしてfindViewByIdの呼び出し回数を減らさなければならないということです。
そこでViewHolderが登場することになります。

ViewHolderAdapterを作成する

findViewByIdの呼び出し回数を減らすために新規作成時にfindViewByIdで取得した値をViewHolderに保持しておき、
二回目以降にViewを使う場合にはViewHolderに保存したViewを使うようにします。
これにてfindViewByIdを呼び出す回数が減りListViewを高速化できます。

今回はPersonクラスのプロパティを表示するTextViewを保持するViewHolderを生成します。

class ViewHolder(var firstNameTextView : TextView, var lastNameTextView : TextView, var ageTextView : TextView)

作成したViewHolderを使ってAdapterを実装します。
一回目だけ新規作成、二回目以降はViewHolderの値を利用するように実装します。

class ViewHolderAdapter(fragmentActivity: FragmentActivity?, resource : Int, list: List<Person>) : ArrayAdapter<Person>(fragmentActivity, resource, list) {
    private val inflater : LayoutInflater = LayoutInflater.from(context)

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        lateinit var convertViewTemp : View
        lateinit var viewHolder : ViewHolder

        if (convertView == null) {
            // Viewを作成
            convertViewTemp = inflater.inflate(kaleidot725.listviewlaboratory.R.layout.list_item, parent, false)
          
            // Viewから必要となるTextViewをfindViewByIdで取り出し、ViewHolderに格納しViewのタグにセット
            val firstNameTextView = convertViewTemp.findViewById<TextView>(kaleidot725.listviewlaboratory.R.id.firstName)
            val lastNameTextView = convertViewTemp.findViewById<TextView>(kaleidot725.listviewlaboratory.R.id.lastName)
            val ageTextView = convertViewTemp.findViewById<TextView>(kaleidot725.listviewlaboratory.R.id.age)
            viewHolder = ViewHolder(firstNameTextView, lastNameTextView, ageTextView)
            convertViewTemp.setTag(viewHolder)
        }
        else {
            // タグにセットしておいたViewHolderを取得し、findViewByIdを使わないでTextViewを取得する
            convertViewTemp = convertView          
            viewHolder = convertViewTemp.getTag() as ViewHolder
        }

        viewHolder.firstNameTextView.text = super.getItem(position).firstName
        viewHolder.lastNameTextView.text = super.getItem(position).lastName
        viewHolder.ageTextView.text = super.getItem(position).age.toString()
        return convertViewTemp
    }
}

3.ListViewを作成する

ここは前回と変わりません。
FramgentのLayoutにListViewを追加し表示できるようにします。

fragment_view_holder

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ViewHolder.ViewHolderFragmenrt">

<ListView
    android:id="@+id/ViewHolderList"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</ListView>
</FrameLayout>

ViewHolderFragmenrt

class ViewHolderFragmenrt : Fragment() {
    val values : List<Person> = mutableListOf(
            Person("Mike",   "Ryan", 15),
            Person("Fred",   "Griffin", 20),
            Person("Ruke",   "Mohammed", 30),
            Person("ふじい",  "まきこ", 40),
            Person("あずま",  "りさ" , 31),
            Person("りゅう",  "ふみえ" , 32),
            Person("とりうみ","のりこ" , 34),
            Person("はすみ",  "ちづる" , 46),
            Person("もりたに","かおり" , 25),
            Person("すがわら","けいすけ" , 36),
            Person("すみよし","たつや" , 40),
            Person("かつやま","まさのり" , 33),
            Person("させ",   "まさあき" , 27),
            Person("ふない",    "ゆうすけ" , 35),
            Person("じんぐうじ", "ただゆき" , 36),
            Person("かねひら",   "ともはる" , 25),
            Person("おぶち",     "こうじ" , 24),
            Person("あいやま",   "やすゆき" , 18))


    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_view_holder_fragmenrt, container, false)
    }

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

        val viewHolderList = view.findViewById<ListView>(R.id.ViewHolderList)
        viewHolderList.adapter = ViewHolderAdapter(activity, android.R.layout.simple_expandable_list_item_1, values)
    }
}

さいごに

このぐらいの表示数ですとそこまでパフォーマンスに影響しないようです。
おそらくですが画像とかをたくさんListViewに配置すると大きな差ができそうです。

f:id:kaleidot725:20180701205642g:plain

github.com