2019-01-05
RecyclerView Adapter 一つに対して複数のViewHolderを持つリストを作成する
Kotlin でAndroidアプリ開発をはじめました。 まず、LegacyとなったListViewの代わりにRecyclerViewというのが新しく用意されているとの事で触ってみました。 ちゃんとしたオブジェクト指向でコードを書くのはかなり久々な事と、 AdapterとHolder、レイアウトファイルの関連が掴めていなかったのもあり、 理解に至るまで苦しみました。 後日順を追ってKotlinによるAndroid開発のメモは残しておきますが、この記事は先に残しておこうと思います。
やりたいこと:Adapter一つに対して複数のViewHolderを持つRecyclerViewを作る
例えば、RecyclerViewのアイテム一覧の一番最後は「+」アイコンで 要素の追加が出来るようなActivityを用意します。
ポイントとなるのは以下の通りです。
onCreateViewHolder
- ViewTypeで一番最後か、そうでないかを判断し、読み込むレイアウトファイルを使い分けます
- 予め用意したViewHolderクラスのスーパークラスにあたる RecyclerView.ViewHolder を戻り値で返します。サブクラス(用意したViewHolderクラス)は返しません
onBindViewHolder
- 引数のPositionから一番最後か、そうでないかを判断し、読み込むViewHolderを使い分けます
- RecyclerView.ViewHolderを引数として待ち受け、読み込むViewHolderに従ってキャストします。 > この時厳密に読み込むViewHolderが一致していないとキャストが出来なかったり、ウィジェットの要素がNullになって落ちるので判断する際は注意する。
レイアウトファイル(xml file)
RecyclerViewを表示するアクティビティ用レイアウトファイル (activity_new_post.xml)
後述する[A]及び[B]を組み合わせたものを期待しています。
xml1<?xml version="1.0" encoding="utf-8"?> 2<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:app="http://schemas.android.com/apk/res-auto" 4 xmlns:tools="http://schemas.android.com/tools" 5 android:layout_width="match_parent" 6 android:layout_height="match_parent" tools:layout_editor_absoluteY="81dp"> 7 8 <android.support.v7.widget.RecyclerView 9 android:id="@+id/addRecyclerView" 10 android:layout_width="match_parent" 11 android:layout_height="545dp" 12 android:layout_marginTop="8dp" 13 app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" 14 android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"/> 15 <Button 16 android:id="@+id/btnAdd" 17 android:text="Post" 18 android:layout_width="match_parent" 19 android:layout_height="wrap_content" 20 android:layout_marginBottom="8dp" 21 app:layout_constraintBottom_toBottomOf="parent" android:layout_marginTop="8dp" 22 app:layout_constraintTop_toBottomOf="@+id/addRecyclerView" app:layout_constraintStart_toStartOf="parent" 23 android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"/> 24</android.support.constraint.ConstraintLayout> 25
[A]RecyclerViewのアイテム一つを表すレイアウトファイル (layout_new_item_card.xml)
CardViewの中にEditTextとImageButtonをおいてみました。予め、専用の画像等は適切なフォルダに入れておいてください。 Image Buttonの使い方を調べておきましょう。
xml1<?xml version="1.0" encoding="utf-8"?> 2<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" 4 android:layout_height="wrap_content" 5 xmlns:app="http://schemas.android.com/apk/res-auto"> 6 7 <android.support.v7.widget.CardView 8 android:id="@+id/android_layout" 9 android:layout_width="match_parent" 10 android:layout_height="wrap_content" 11 app:cardBackgroundColor="@android:color/white"> 12 13 <android.support.constraint.ConstraintLayout 14 android:layout_width="match_parent" 15 android:layout_height="wrap_content"> 16 17 <EditText 18 android:id="@+id/edtFlowerName" 19 android:layout_width="wrap_content" 20 android:layout_height="wrap_content" 21 android:inputType="textPersonName" 22 android:hint="花名を入力" 23 android:ems="20" /> 24 25 <ImageButton 26 android:id="@+id/ibtnCamera" 27 android:layout_width="wrap_content" 28 android:layout_height="wrap_content" 29 app:srcCompat="@android:drawable/ic_menu_camera" 30 app:layout_constraintTop_toBottomOf="@+id/edtFlowerName" /> 31 32 </android.support.constraint.ConstraintLayout> 33 </android.support.v7.widget.CardView> 34</android.support.constraint.ConstraintLayout> 35
[B]RecyclerViewの一番最後の要素に表示するレイアウトファイル (layout_add_button.xml)
Image Buttonで+アイコンのみ配置しています
xml1<?xml version="1.0" encoding="utf-8"?> 2<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:app="http://schemas.android.com/apk/res-auto" 4 xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" 5 android:layout_height="wrap_content"> 6 7 <ImageButton 8 android:id="@+id/ibtnItemAdd" 9 android:layout_width="wrap_content" 10 android:layout_height="wrap_content" app:srcCompat="@android:drawable/ic_menu_add" 11 app:layout_constraintEnd_toEndOf="parent" 12 android:layout_marginEnd="8dp" app:layout_constraintStart_toStartOf="parent" 13 android:layout_marginStart="8dp" android:layout_marginTop="8dp" app:layout_constraintTop_toTopOf="parent" 14 android:layout_marginBottom="8dp" app:layout_constraintBottom_toBottomOf="parent"/> 15</android.support.constraint.ConstraintLayout> 16
Holder
ViewHolder A (NewPostViewHolder.kt)
Java1package com.killinsun.app.recyclepractice 2 3import android.support.v7.widget.RecyclerView 4import android.view.View 5import android.widget.EditText 6import android.widget.ImageButton 7 8class NewPostViewHolder(view: View) : RecyclerView.ViewHolder(view) { 9 10 // 最後の行以外の場合はこっちを読み込む。 11 val edtFlowerName: EditText? = view.findViewById(R.id.edtFlowerName) 12 val ibtnCamera: ImageButton? = view.findViewById(R.id.ibtnCamera) 13 14} 15
ViewHolder B (NewItemViewHolder.kt)
Java1package com.killinsun.app.recyclerpractice 2 3import android.support.v7.widget.RecyclerView 4import android.view.View 5import android.widget.ImageButton 6 7class NewItemViewHolder(view: View) : RecyclerView.ViewHolder(view){ 8 9 // 最後の行だった場合はこっちを読み込む 10 val ibtnItemAdd: ImageButton = view.findViewById(R.id.ibtnItemAdd) 11} 12
Adapter(NewPostAdapter.kt)
Java1package com.killinsun.app.recyclepractice 2 3import android.support.v7.widget.RecyclerView 4import android.util.Log 5import android.view.LayoutInflater 6import android.view.View 7import android.view.ViewGroup 8import android.widget.ArrayAdapter 9import kotlinx.android.synthetic.main.layout_new_item_card.view.* 10 11class NewPostAdapter(private val itemList:ArrayList<Int>) 12 : RecyclerView.Adapter<RecyclerView.ViewHolder>(){ 13 14 override fun getItemViewType(position: Int) : Int{ 15 return if(position == itemList.size) 0 else 1 // ① 16 } 17 18 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 19 20 val layoutInflater = LayoutInflater.from(parent.context) 21 val view: View // ② 22 23 // ③ 24 if(viewType == 0){ 25 //最後の行の場合は追加ボタンレイアウトを読み込む 26 view = layoutInflater.inflate(R.layout.layout_add_button, parent, false) 27 return NewItemViewHolder(view) 28 }else { 29 //最後の行以外の場合はそのままカードを読み込む 30 view = layoutInflater.inflate(R.layout.layout_new_item_card, parent, false) 31 return NewPostViewHolder(view) 32 } 33 34 } 35 36 override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 37 // ④ 38 if(position == itemList.size){ 39 val myHolder: NewItemViewHolder = holder as NewItemViewHolder 40 myHolder.ibtnItemAdd.setOnClickListener{ 41 Log.v("test","Add button clicked!!!") 42 itemList.add(position+1) 43 notifyItemInserted(position + 1) 44 } 45 }else { 46 val myHolder: NewPostViewHolder = holder as NewPostViewHolder 47 myHolder.ibtnCamera?.setOnClickListener { 48 Log.v("test", "Camera button clicked!!!") 49 } 50 } 51 52 } 53 54 override fun getItemCount(): Int{ 55 return itemList.size + 1 // ⑤ 56 } 57 58} 59
①
getItemView
メソッドでポジションを受け取り、用意したitemListとサイズが一致(=与えられた要素の位置が最終行)の場合のみ0を返却し、それ以外の行(=与えられた要素の位置が最終行以外)の場合は、1を返却します。
②
予め View型のオブジェクトを用意しておきます。
③
getItemViewType
で得られた戻り値(0 or 1)を基に判断を進めます。 上述の通り0の場合は最終行なのでそれ用のレイアウトファイルをlayoutInflater
メソッドにより読み込み、②で宣言したViewオブジェクトに代入します。その後、メソッドとしてViewHolder
クラスを戻り値として返して終了します。
この時返却するViewHolder
クラスは、要素に応じたものです。
最終行以外の場合も、読み込むレイアウトファイルと戻り値のViewHolder
クラスが違うだけで、やっていることは一緒です。
④
これまでと同様に、今回はpositionの値によって最終行かそうでないかを判断しています。
myHolderに対しては、最終行だったら引数として読み込まれた holder
(RecycleView.ViewHolder
型)を NewItemViewHolder
型にキャストといった処理をしています。最終行以外の場合も、相応の処理を行っています。
また、この時別々のクラス型にキャストしてあげる事で、ViewHolder
内で宣言したウィジェット(例えば最終要素の場合は+ボタン)のsetOnClickListener
にアクセス出来るようになっています。
ちなみに、今回は+ボタンを押された際にRecyclerViewの要素が1つ増える処理が記載されています。
⑤
おまじないです。 このRecyclerViewは要素数+1は常に「+ボタン」になっています。
Activity
Java1package com.killinsun.app.recyclerpractice 2 3import android.os.Bundle 4import android.support.v7.app.AppCompatActivity 5import android.support.v7.widget.LinearLayoutManager 6import kotlinx.android.synthetic.main.activity_new_post.* 7 8class NewPostActivity : AppCompatActivity(){ 9 10 override fun onCreate(savedInstanceState: Bundle?){ 11 super.onCreate(savedInstanceState) 12 setContentView(R.layout.activity_new_post) 13 14 var listItems: ArrayList<Int> = arrayListOf(0) 15 16 addRecyclerView.layoutManager = LinearLayoutManager(this) 17 addRecyclerView.adapter = NewPostAdapter(listItems) 18 19 } 20} 21
今回のアクティビティではユーザの操作によってRecyclerViewの要素が増える事を想定したものの為、Adapterに渡す配列はInt型で1要素のみを渡しています。
やってみて
ココ最近のAndroidアプリ開発は右も左も分からないまま突っ込んで、ようやくAdapterとHolderの仕組みが少し理解できたかなという気持ちです。 最初は戻り値となるViewHolderをサブクラス(NewItemViewHolder)に指定してたりして、「どうやってもう片方のViewHolderの受け取るんだよ」って躓いたりしました。スーパークラスにしておけばいいんですね・・・。
正直、上記の記述がベストプラクティスだとは思っていませんし、改善の余地があるかと思いますが、 ひとまず「動くもの」を目指して作るのは大事な事だと思っています。 その上でリファクタリングして更に理解を深められたらいいな。
いずれにしてもAdapterにあまり処理を書きたくないと思っているので、どこに移動させるのがベストか、 意見がありましたらツイッターでもコメントでもフィードバックいただけると嬉しいです。
参考
/以上
よかったらシェアしてください!