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を表示するアクティビティ用レイアウトファイル (activitynewpost.xml)
後述する[A]及び[B]を組み合わせたものを期待しています。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:layout_editor_absoluteY="81dp">
<android.support.v7.widget.RecyclerView
android:id="@+id/addRecyclerView"
android:layout_width="match_parent"
android:layout_height="545dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"/>
<Button
android:id="@+id/btnAdd"
android:text="Post"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@+id/addRecyclerView" app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"/>
</android.support.constraint.ConstraintLayout>
[A]RecyclerViewのアイテム一つを表すレイアウトファイル (layoutnewitem_card.xml)
CardViewの中にEditTextとImageButtonをおいてみました。予め、専用の画像等は適切なフォルダに入れておいてください。 Image Buttonの使い方を調べておきましょう。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.v7.widget.CardView
android:id="@+id/android_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@android:color/white">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/edtFlowerName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:hint="花名を入力"
android:ems="20" />
<ImageButton
android:id="@+id/ibtnCamera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@android:drawable/ic_menu_camera"
app:layout_constraintTop_toBottomOf="@+id/edtFlowerName" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
</android.support.constraint.ConstraintLayout>
[B]RecyclerViewの一番最後の要素に表示するレイアウトファイル (layoutaddbutton.xml)
Image Buttonで+アイコンのみ配置しています
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="wrap_content">
<ImageButton
android:id="@+id/ibtnItemAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content" app:srcCompat="@android:drawable/ic_menu_add"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp" app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" android:layout_marginTop="8dp" app:layout_constraintTop_toTopOf="parent"
android:layout_marginBottom="8dp" app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>
Holder
ViewHolder A (NewPostViewHolder.kt)
package com.killinsun.app.recyclepractice
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.EditText
import android.widget.ImageButton
class NewPostViewHolder(view: View) : RecyclerView.ViewHolder(view) {
// 最後の行以外の場合はこっちを読み込む。
val edtFlowerName: EditText? = view.findViewById(R.id.edtFlowerName)
val ibtnCamera: ImageButton? = view.findViewById(R.id.ibtnCamera)
}
ViewHolder B (NewItemViewHolder.kt)
package com.killinsun.app.recyclerpractice
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.ImageButton
class NewItemViewHolder(view: View) : RecyclerView.ViewHolder(view){
// 最後の行だった場合はこっちを読み込む
val ibtnItemAdd: ImageButton = view.findViewById(R.id.ibtnItemAdd)
}
Adapter(NewPostAdapter.kt)
package com.killinsun.app.recyclepractice
import android.support.v7.widget.RecyclerView
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import kotlinx.android.synthetic.main.layout_new_item_card.view.*
class NewPostAdapter(private val itemList:ArrayList<Int>)
: RecyclerView.Adapter<RecyclerView.ViewHolder>(){
override fun getItemViewType(position: Int) : Int{
return if(position == itemList.size) 0 else 1 // ①
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view: View // ②
// ③
if(viewType == 0){
//最後の行の場合は追加ボタンレイアウトを読み込む
view = layoutInflater.inflate(R.layout.layout_add_button, parent, false)
return NewItemViewHolder(view)
}else {
//最後の行以外の場合はそのままカードを読み込む
view = layoutInflater.inflate(R.layout.layout_new_item_card, parent, false)
return NewPostViewHolder(view)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
// ④
if(position == itemList.size){
val myHolder: NewItemViewHolder = holder as NewItemViewHolder
myHolder.ibtnItemAdd.setOnClickListener{
Log.v("test","Add button clicked!!!")
itemList.add(position+1)
notifyItemInserted(position + 1)
}
}else {
val myHolder: NewPostViewHolder = holder as NewPostViewHolder
myHolder.ibtnCamera?.setOnClickListener {
Log.v("test", "Camera button clicked!!!")
}
}
}
override fun getItemCount(): Int{
return itemList.size + 1 // ⑤
}
}
①
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
package com.killinsun.app.recyclerpractice
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_new_post.*
class NewPostActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_post)
var listItems: ArrayList<Int> = arrayListOf(0)
addRecyclerView.layoutManager = LinearLayoutManager(this)
addRecyclerView.adapter = NewPostAdapter(listItems)
}
}
今回のアクティビティではユーザの操作によってRecyclerViewの要素が増える事を想定したものの為、Adapterに渡す配列はInt型で1要素のみを渡しています。
やってみて
ココ最近のAndroidアプリ開発は右も左も分からないまま突っ込んで、ようやくAdapterとHolderの仕組みが少し理解できたかなという気持ちです。 最初は戻り値となるViewHolderをサブクラス(NewItemViewHolder)に指定してたりして、「どうやってもう片方のViewHolderの受け取るんだよ」って躓いたりしました。スーパークラスにしておけばいいんですね・・・。
正直、上記の記述がベストプラクティスだとは思っていませんし、改善の余地があるかと思いますが、 ひとまず「動くもの」を目指して作るのは大事な事だと思っています。 その上でリファクタリングして更に理解を深められたらいいな。
いずれにしてもAdapterにあまり処理を書きたくないと思っているので、どこに移動させるのがベストか、 意見がありましたらツイッターでもコメントでもフィードバックいただけると嬉しいです。
参考
/以上