RecyclerViewはアイテムへ装飾を付けることが出来るようになっています。
装飾とは、例えばアイテムの区切り線などです。
今回はアイテムへ装飾を付ける方法を紹介します。
目次
RecyclerViewはアイテムを並べるだけ
RecyclerViewはアイテムを並べるだけです。デフォルトはそれ以外のことを行いません。


左のようにデフォルトはアイテムの境界が曖昧です。右のように区切り線を入れると、アイテムの区別が付きやすくなります。
この区切り線はアイテムへ装飾を付けることで表現されています。
装飾の付け方
ItemDecorationの実装と登録
アイテムの装飾はItemDecorationクラスを実装して、RecyclerViewへ登録すれば行われます。
RecyclerViewと同サイズのCanvasが2枚あり(RecyclerViewの上に表示、下に表示)、そのCanvasに描いた内容がRecyclerViewに重ねて表示されるという仕組みです。
アイテム毎に装飾を付けたければ、Canvas中のアイテムの位置を算出しなければなりません。この点が少々面倒です。
...
rcySample = findViewById<RecyclerView>(R.id.rcySample)
val _deco = object : RecyclerView.ItemDecoration() { // ItemDecorationの実装
override fun onDraw(
c: Canvas, parent: RecyclerView,
state: RecyclerView.State) {
// Canvasへ装飾を描画
// アイテムが描画される前に描画されるため、アイテムの下に表示
}
override fun onDrawOver(
c: Canvas, parent: RecyclerView,
state: RecyclerView.State) {
// Canvasへ装飾を描画
// アイテムが描画された後に描画されるため、アイテムの上に表示
}
override fun getItemOffsets(
outRect: Rect, view: View, parent: RecyclerView,
state: RecyclerView.State) {
// アイテムの上下左右へ空間を設ける
// outRect.set(leftの空間, topの空間, rightの空間, bottomの空間)
}
}
rcySample.addItemDecoration(_deco) // ItemDecorationの登録
...
2枚あるCanvasのどちらに装飾を描画するかでRecyclerViewとの上下関係が変わってきます。これはスクリーンへ描画する順番が違うためです。

また、getItemOffsetsでoutRect(Rect型)引数へ値を設定すれば、アイテムの周りに空白(透明)を設けることが出来ます。
空白のサイズ指定はピクセル(px)です。
アイテムのViewのサイズはこの空白も含めたサイズになるので注意してください。

ItemDecorationは複数登録できる
ItemDecorationは複数登録でき、登録された順番で実行されます。
...
rcySample = findViewById<RecyclerView>(R.id.rcySample)
...
val _deco0 = object : RecyclerView.ItemDecoration() {
override fun onDraw(
c: Canvas, parent: RecyclerView,
state: RecyclerView.State) {
Log.i("ItemDecoration", "onDraw (0)")
}
}
val _deco1 = object : RecyclerView.ItemDecoration() {...}
val _deco2 = object : RecyclerView.ItemDecoration() {...}
val _decoX = object : RecyclerView.ItemDecoration() {...}
rcySample.addItemDecoration(_deco0)
rcySample.addItemDecoration(_deco1)
rcySample.addItemDecoration(_deco2)
rcySample.addItemDecoration(_decoX, 1) // 第2引数はインデックス番号
...
... I/ItemDecoration: onDraw (0) ... I/ItemDecoration: onDraw (X) ... I/ItemDecoration: onDraw (1) ... I/ItemDecoration: onDraw (2)
RecyclerViewの記述を参照すると、ItemDecorationはArrayListで管理されていることが分かります。
つまり、ItemDecorationはArrayListから1つずつ取り出して実行されるのです。
...
final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<>();
...
public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
...
if (index < 0) {
mItemDecorations.add(decor);
} else {
mItemDecorations.add(index, decor);
}
markItemDecorInsetsDirty();
requestLayout();
}
...
public void addItemDecoration(@NonNull ItemDecoration decor) {
addItemDecoration(decor, -1);
}
...
装飾の例:区切り線(DividerItemDecoration)
区切り線を入れたい場合は、DividerItemDecorationという既成のItemDecorationがAPIに用意されています。
DividerItemDecorationを用いればデフォルトの区切り線が装飾されます。
...
rcySample = findViewById<RecyclerView>(R.id.rcySample)
...
val _deco = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
rcySample.addItemDecoration(_deco)
...

注意点は区切り線の高さ(幅)の分だけアイテムのViewのサイズが大きくなることです。
よって、固定サイズのRecyclerViewの場合、RecyclerView中に表示されるアイテムの数が変化してしまいます。※サンプルはItem 5の高さが低くなった
装飾のテクニック(DividerItemDecorationの解説)
DividerItemDecorationのソースコードを解析すると、参考になるテクニックが満載です。
コンストラクタ
デフォルトの区切り線をmDivider(Drawable型)フィールドに代入しています。
...
private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };
private Drawable mDivider;
...
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
if (mDivider == null) {
Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this "
+ "DividerItemDecoration. Please set that attribute all call setDrawable()");
}
a.recycle();
setOrientation(orientation);
}
...
デフォルトの区切り線はAndroidシステムのリソースに定義されているandroid.R.attr.listDividerです。
listDividerはxmlで定義されたDrawableで、高さ1[dp](幅1[dp])になっています。
<?xml version="1.0" encoding="utf-8"?>
...
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorListDivider">
<solid android:color="?attr/opacityListDivider" />
<size
android:height="1dp"
android:width="1dp" />
</shape>
xmlで定義されたDrawableはGradientDrawableクラスに変換されるので、実際にmDividerに代入されるのはGradientDrawableのインスタンスです。
getItemOffsets
区切り線を表示するスペース(空白)を作っています。
...
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mDivider == null) {
outRect.set(0, 0, 0, 0);
return;
}
if (mOrientation == VERTICAL) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
...

Drawable#getIntrinsicHeightは区切り線の高さをピクセル単位で返します。
※mdpiなら1[dp]⇒1[px]、hdpiなら1[dp]⇒1.5[px]、xhdpiなら1[dp]⇒2[px]へ変換
この値をoutRect.set(Rect#set)のbottomに指定しているので、アイテムのViewの下側に、区切り線の高さ分のスペースが作られます。
onDraw
次にあげる2つのことを行っています。
- 区切り線の描画範囲を判定 … if文の部分
- アイテムの座標を取得、区切り線を描画 … for分の部分
...
private final Rect mBounds = new Rect();
...
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null || mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
final int left;
final int right;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) { // (1)
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right,
parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) { // (2)
final View child = parent.getChildAt(i); // アイテムのViewを取り出し
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom); // 区切り線を描画
mDivider.draw(canvas); // 同上
}
canvas.restore();
}
...
区切り線の描画範囲を判定 …(1)
RecyclerView#getClipToPaddingは、アイテムがスクロールされる時のPadding領域の扱い方を示しています。
trueであれば、Padding領域を画面へ留めます(デフォルト)。
falseであれば、Padding領域をスクロールします。
この両者の違いによってスクロールされる範囲が異なってきます。
区切り線はアイテムに従属するものです。アイテムのスクロールする範囲が変われば、区切り線の描画範囲も同様に変わります。
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rcySample"
android:layout_width="200dp"
android:layout_height="240dp"
android:layout_marginTop="40dp"
android:background="@android:color/darker_gray"
android:paddingLeft="10dp"
android:paddingTop="20dp"
android:paddingRight="10dp"
android:paddingBottom="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />


アイテムの座標を取得、区切り線を描画 …(2)
RecyclerViewはVewGroupを継承したコンテナタイプのViewです。
ViewGroupは内部に持つ子Viewをインデックス番号を指定して取り出せます。RecyclerViewの子ViewとはアイテムのViewのことです。
forループでインデックス番号を順に送りながらアイテムのViewを取り出して行きます。

取り出されたアイテムのViewはRecyclerView上の座標が取得されます。その座標を素にアイテムの下部へ区切り線を描画します。

アニメーションに追従
アイテムのポジションが変わるような変更を行うと、変更の様子がアニメーションで表現されます。
区切り線がアニメーションへ追従するように、アイテムの位置変化量(child.getTranslationY)を座標へ加算する処理が入っています。
...
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
...
以下に、アイテムの位置変化量の加算なし・ありの動作を示します。
ポジション1から3へアイテムが移動しています。また、違いが分かるように区切り線を太くしています。
ポジション3のアイテムが消える時に区切り線が取り残されている。
ポジション3のアイテムが消える時に区切り線が同時に消えている。
関連記事:
