こんばんは、最近Androidエンジニアを名乗っていいのか悩ましいid:takuji31です。
今日はRecyclerViewのItemDecorationについて簡単な使い方をまとめてみました。
なお、この記事の一部は本日開催の関西モバイルアプリ研究会#20で発表された内容です。
RecyclerView.ItemDecoration とは
RecyclerViewの各アイテムに対して何らかの装飾を施す仕組みです。
代表的なのは罫線を引いてくれる(Support Library25.0.0でようやく追加された)DividerItemDecorationやアイテムのDnDやスワイプが行えるようになるめっちゃ便利なItemTouchHelperがあります(というより公式で提供されている子クラスはこの2つだけ)。
色んなRecyclerViewのアイテムに対して簡単な装飾を施したいのなら、レイアウトで頑張るよりこちらでやる方がよいかと思います。
メソッド
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
指定したViewが本来レイアウトした位置からどれくらいズレるのかを指定するメソッドです。
最後の state
がないメソッドがありますが、これは非推奨です。
view
が対象のView、 outRect
がアイテムのオフセットを返すための Rect
です、 outRect
の left|top|right|bottom
に直接値をセットします。
onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)
装飾の描画時に呼ばれるメソッドです。呼ばれるタイミングは各アイテムの内容が描画される前です。
最後の state
がないメソッドがありますが、これは非推奨です。
c
はRecyclerView全体のCanvasです、各要素に対して呼ばれるのではなく、描画ごとに1回だけ呼ばれます。
このメソッドで装飾を描画することになります。
onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)
基本的には onDraw
と同じですが、こちらは呼び出されるタイミングがアイテムの内容が描画された後です。
最後の state
がないメソッドがありますが、これは非推奨です。
アイテムに重ねて描画したい場合に使うとよさそうです。
使い方
基本的には ItemDecoration
を継承したクラスを作って、getItemOffset
と onDraw
もしくは onDrawOver
(あるいは両方)を実装してやるとよいです。
使う時はRecyclerView.addItemDecoration(RecyclerView.ItemDecoration decor)
を呼んで装飾を追加します。
今回はiOSの UITableViewCell
にある accessoryType
のようなものを作ってみました。
AccessoryTypeを定義する
雑にenumを定義しました、とりあえず2タイプだけサポート。
enum class AccessoryType(@DrawableRes val resId: Int) { NONE(0), DISCLOSURE_INDICATOR(R.drawable.ic_chevron_right_black_24dp), CHECK_MARK(R.drawable.ic_done_black_24dp), }
Adapterを作る
文字列と AccessoryType
を持つ Pair
のリストを持つだけの Adapter
を作りました。
ポイントは ViewHolder.itemView
にsetTag
で AccessoryType
を渡していること。
View
と RecyclerView
から ViewHolder
を取り出すことはできますが、これだと ViewHolder
の型に ItemDecoration
が依存してしまうので、Tagで渡しています。
class Adapter(var items: List<Pair<String, AccessoryType>>) : RecyclerView.Adapter<ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val inflator = LayoutInflater.from(parent.context) return ViewHolder(view = inflator.inflate(R.layout.recyler_row_simple_textview, parent, false)) } override fun getItemCount(): Int { return items.size } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val (text, accessoryType) = items[position] holder.textView.text = text holder.textView.setOnClickListener { Log.d(javaClass.simpleName, "clicked") } holder.itemView.setTag(R.id.tagAccessoryType, accessoryType) } }
ItemDecorationを作る
今回は AccessoryType.NONE
以外の時に右側にいい感じに装飾を表示することにしました。
ItemDecoration
全体のコードは↓の通り
class AccessoryItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() { val horizontalMargin by lazy { context.resources.getDimensionPixelOffset(R.dimen.accessory_horizontal_margin) } override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val accessoryType = view.getTag(R.id.tagAccessoryType) as? AccessoryType if (accessoryType != null) { if (accessoryType != AccessoryType.NONE) { val drawable = ContextCompat.getDrawable(view.context, accessoryType.resId) outRect.right = drawable.intrinsicWidth + horizontalMargin * 2 } } } override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { val childCount = parent.childCount for (i in 0..childCount - 1) { val child = parent.getChildAt(i) val accessoryType = child.getTag(R.id.tagAccessoryType) as? AccessoryType if (accessoryType != null && accessoryType != AccessoryType.NONE) { val drawable = ContextCompat.getDrawable(child.context, accessoryType.resId) val bounds = Rect() parent.getDecoratedBoundsWithMargins(child, bounds) val left = bounds.right - horizontalMargin - drawable.intrinsicWidth val right = left + drawable.intrinsicWidth val top = bounds.top + ((bounds.bottom - bounds.top - drawable.intrinsicHeight) / 2) + Math.round(ViewCompat.getTranslationY(child)) val bottom = top + drawable.intrinsicHeight drawable.setBounds(left, top, right, bottom) drawable.draw(c) } } } }
getItemOffsets
ViewHolder
でセットした AccessoryType
のTagを取得して、修飾に使うDrawableとmargin分だけ右側にoffsetを設定しています。
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val accessoryType = view.getTag(R.id.tagAccessoryType) as? AccessoryType if (accessoryType != null) { if (accessoryType != AccessoryType.NONE) { val drawable = ContextCompat.getDrawable(view.context, accessoryType.resId) outRect.right = drawable.intrinsicWidth + horizontalMargin * 2 } } }
onDrawOver
子の View
を1つずつ取得して、それぞれ必要に応じて Drawable
を描画しています。
parent.getDecoratedBoundsWithMargins(child, bounds)
でそのViewの装飾込みの境界を表すRectが取得できます。
ViewCompat.getTranslationY(child)
でViewのtranslationYが取得できるので、この分描画位置をずらす必要があります。
あとは Drawable
を Canvas
に描画するだけです。
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { val childCount = parent.childCount for (i in 0..childCount - 1) { val child = parent.getChildAt(i) val accessoryType = child.getTag(R.id.tagAccessoryType) as? AccessoryType if (accessoryType != null && accessoryType != AccessoryType.NONE) { val drawable = ContextCompat.getDrawable(child.context, accessoryType.resId) val bounds = Rect() parent.getDecoratedBoundsWithMargins(child, bounds) val left = bounds.right - horizontalMargin - drawable.intrinsicWidth val right = left + drawable.intrinsicWidth val top = bounds.top + ((bounds.bottom - bounds.top - drawable.intrinsicHeight) / 2) + Math.round(ViewCompat.getTranslationY(child)) val bottom = top + drawable.intrinsicHeight drawable.setBounds(left, top, right, bottom) drawable.draw(c) } } }
スクリーンショット
このコードの問題点
- Offsetを指定してる分選択した時の背景描画が小さくなる
- もちろんdrawしているだけなので、その装飾をタップできたりはしない
今回は分かりやすい例を出すためにこんな感じにしましたが、こういうのはViewに直接レイアウトした方がいいかもしれません。
まとめ
getItemOffsets
でoffsetを決めるonDraw
かonDrawOver
で描画するRecyclerView.addItemDecoration()
して使う
今回のサンプルコードはこちら