00.経緯
仕事で「リストの表示をPinterrest風にしたい」という要望がありました。
雑に「android pinterest layout」とググってみたところ日本語のページが引っかからなかったので、メモしておきます。
なお、これを書いている際になんとなく順番を入れ替えて「pinterest layout android」でググってみたところ以下が引っかかりました。
なんでや
01. Gradleへの変更
RecyclerViewを使用するので以下を追加します。
dependencies {
implementation "androidx.recyclerview:recyclerview:1.1.0"
}
02. Gridに表示するViewのレイアウトを作成
高さを可変(wrap_content)にしておくことが重要です。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#00ffff"
android:gravity="center"
android:padding="5dp" />
</LinearLayout>
03. ViewHolderの作成
RecyclerView#ViewHolderを継承したViewHolderを作成します。
class MasonryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private var imageView: ImageView = itemView.findViewById(R.id.image_view)
private var textView: TextView = itemView.findViewById(R.id.text_view)
fun setImage(@DrawableRes resId: Int) {
imageView.setImageResource(resId)
}
fun setText(text: String) {
textView.text = text
}
}
04. Adapterの作成
RecyclerView#Adapterを継承した(ry
class MasonryAdapter(context: Context, private val items: List<Pair<Int, String>>): RecyclerView.Adapter<MasonryViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MasonryViewHolder {
val view = inflater.inflate(R.layout.view_masonry_item, parent, false)
return MasonryViewHolder(view)
}
override fun getItemCount(): Int {
return items.count()
}
override fun onBindViewHolder(holder: MasonryViewHolder, position: Int) {
holder.setImage(items[position].first)
holder.setText(items[position].second)
}
}
05. RecyclerViewにLayoutManager及びAdapterを適用する
LayoutManagerにStaggeredGridLayoutManagerを使用します。
※ createListItems()は表示要素の配列を作成しているだけです。
recycler_view.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
recycler_view.adapter = MasonryAdapter(this, createListItems())

06. 要素間にスペースを設ける
ここまででPinterest風なGridViewにはなっていますが、要素間にスペースがありません。
要素のViewにスペースを設けても良いですが、ここはRecyclerView#ItemDecorationを使用してみます。
class MasonryItemDecoration : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.apply {
left = 16
right = 16
bottom = 16
// 2重にスペースを設けてしまうので、最上段のみ上部にスペースを設けるようにする
if (parent.getChildAdapterPosition(view) <= 1) {
top = 16
}
}
}
}
recycler_view.addItemDecoration(MasonryItemDecoration())

07. 要素へのタップを可能にする
リストが表示されればOKということはそうそうないでしょうから、要素をタップできるようにしておきます。
Adapterを少しいじります。
※ 前述部分と変更箇所のみ記載
class MasonryAdapter(context: Context, private val items: List<Pair<Int, String>>): RecyclerView.Adapter<MasonryViewHolder>() {
interface MasonryItemClickListener {
fun onClick(position: Int)
}
var masonryItemClickListener: MasonryItemClickListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MasonryViewHolder {
val view = inflater.inflate(R.layout.view_masonry_item, parent, false)
val holder = MasonryViewHolder(view)
holder.itemView.setOnClickListener {
masonryItemClickListener?.onClick(holder.adapterPosition)
}
return holder
}
}
recycler_view.adapter = MasonryAdapter(this, createListItems()).apply {
masonryItemClickListener = object : MasonryAdapter.MasonryItemClickListener {
override fun onClick(position: Int) {
Toast.makeText(this@MainActivity, "click position: $position", Toast.LENGTH_SHORT).show()
}
}
}
