この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
レイアウトを折りたたみ可能にするライブラリ「ExpandableLayout」を使って、折りたたみ可能なリストを作ってみました。
はじめに
折りたたみ可能なリストをExpandableLayoutとRecyclerViewを使って実装します。
今回実装したソースコードはこちら↓
開発環境
- OS: MacOS Catalina
- Android Studio: 4.1.2
- Language: Kotlin 1.4.21
手順
- ライブラリを追加する
- レイアウトファイルの編集・追加
- Adapterの作成と実装
- MainActivityの実装
アプリレベルのbuild.gradleにライブラリを追加
ExpandableLayout、RecyclerView、CardViewのライブラリを追加します。
app/build.gradle
dependencies {
// 省略
implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2'
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
}
レイアウトの編集(MainActivity)
activity_main.xmlにRecyclerViewを追加します。
src/main/res/layout/activity_main.xml
<?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=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="16dp"
app:layoutManager="LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
レイアウトの追加(RecyclerViewのアイテム)
RecylerViewに表示するアイテムのレイアウトを新規作成します。ExpandableLayoutを使って折りたたみ可能なレイアウトを定義します。開閉のアニメーションが良い感じになるようにapp:el_durationでアニメーションの伸縮時間を300msに、app:el_parallaxでパララックスを0.3で指定しました。
src/main/res/layout/recycler_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp"
app:cardUseCompatPadding="true"
card_view:cardCornerRadius="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/title_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="16dp" />
<ImageView
android:id="@+id/expand_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="12dp"
app:srcCompat="@drawable/expand_arrow" />
</LinearLayout>
<net.cachapa.expandablelayout.ExpandableLayout
android:id="@+id/expandable_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:el_duration="300"
app:el_parallax="0.3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:srcCompat="@mipmap/ic_launcher" />
<TextView
android:id="@+id/detail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="center"
android:padding="16dp" />
</LinearLayout>
</net.cachapa.expandablelayout.ExpandableLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
タップした時の矢印アイコンも準備しておきます。矢印アイコンの画像ファイルは各自で準備してください。
src/main/res/drawable/expand_arrow.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_arrow_down_24px" android:state_selected="true" />
<item android:drawable="@drawable/ic_arrow_up_24px" android:state_selected="false" />
</selector>
Adapterの実装 その1(リストに表示するデータを表すMyItem)
RecyclerViewで指定するAdapterを実装していきます。まず、リストに表示するデータを表すMyItemクラスを実装します。項目は、id、タイトル、詳細情報、開閉状態の4つとします。
src/main/java/com/mos1210/android/example/expandablelayout/MyItem.kt
data class MyItem(
val id: Int,
val title: String,
val detail: String,
var isExpanded: Boolean = false
)
Adapterの実装 その2(Viewを再利用するためのViewHolder)
MyAdapterクラスを新規作成して、その中にRecyclerView.ViewHolderを継承したMyViewHolderクラスを実装します。リストアイテムタップ時のアニメーションと拡大縮小アイコンを回転させるアニメーションもここで実装します。アニメーションはタップ時のみに限定したいのでbind関数の中ではexpand関数をfalseで呼び出します。これで、リストのアイテムが開いた状態でスクロールしてもアニメーションOFFで表示されます。
src/main/java/com/mos1210/android/example/expandablelayout/MyAdapter.kt
class MyViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
private var currentItem: MyItem? = null
private val title: TextView = itemView.findViewById(R.id.title)
private val detail: TextView = itemView.findViewById(R.id.detail)
private val expandedLayout: ExpandableLayout = itemView.findViewById(R.id.expandable_layout)
private val titleLayout: LinearLayout = itemView.findViewById(R.id.title_layout)
private val arrow: ImageView = itemView.findViewById(R.id.expand_arrow)
init {
itemView.setOnClickListener {
currentItem?.let {
val expanded = it.isExpanded
it.isExpanded = expanded.not()
titleLayout.isSelected = expanded.not()
expandedLayout.toggle()
val anim = RotateAnimation(
0f,
180f,
Animation.RELATIVE_TO_SELF,
0.5f,
Animation.RELATIVE_TO_SELF,
0.5f
).apply {
duration = 300
fillAfter = true
}
arrow.startAnimation(anim)
}
}
}
fun bind(item: MyItem) {
currentItem = item
title.text = currentItem?.title
detail.text = currentItem?.detail
if (item.isExpanded) {
expandedLayout.expand(false)
} else {
expandedLayout.collapse(false)
}
titleLayout.isSelected = item.isExpanded.not()
}
}
Adapterの実装
MyAdapterにListAdapterを継承し、onCreateViewHolder及びonBindViewHolderを実装します。
class MyAdapter :
ListAdapter<MyItem, MyAdapter.MyViewHolder>(MyDiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.recycler_item, parent, false)
)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item: MyItem = getItem(position)
holder.bind(item)
}
class MyViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
// 省略 前節で実装紹介したため
}
}
object MyDiffCallback : DiffUtil.ItemCallback<MyItem>() {
override fun areItemsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
return oldItem.id == newItem.id
}
}
MainActivityでリスト表示の実装
MainAcitivityでRecyclerViewのデータ生成とAdapterを指定します。ListAdapterクラスのsubmitListメソッドで作成したデータを投入します。
src/main/java/com/mos1210/android/example/expandablelayout/MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val list: MutableList<MyItem> = mutableListOf()
for (i in 0..50) {
list.add(MyItem(i, "title $i", "detail $i"))
}
val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
val myAdapter = MyAdapter()
recyclerView.adapter = myAdapter
myAdapter.submitList(list)
}
}
実行結果
アプリを実行すると画面が表示されます。
リストアイテムをタップするとアニメーション付きでアイテムが開きます。
まとめ
ExpandableLayoutとRecyclerViewを組み合わせて折りたたみ可能なリスト表示ができました。もっと簡単で良さそうな方法があるよ!などあればTwitterやコメントで教えていただければ嬉しいです。