RecyclerView:アイテムの装飾(ItemDecoration)

投稿日:  更新日:

RecyclerViewはアイテムへ装飾を付けることが出来るようになっています。

装飾とは、例えばアイテムの区切り線などです。

今回はアイテムへ装飾を付ける方法を紹介します。

スポンサーリンク

RecyclerViewはアイテムを並べるだけ

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との上下関係が変わってきます。これはスクリーンへ描画する順番が違うためです。

DecorationのCanvas

また、getItemOffsetsでoutRect(Rect型)引数へ値を設定すれば、アイテムの周りに空白(透明)を設けることが出来ます。

空白のサイズ指定はピクセル(px)です。

アイテムのViewのサイズはこの空白も含めたサイズになるので注意してください。

DecorationのOffset

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)
		...

RecyclerViewに区切り線を付けた

注意点は区切り線の高さ(幅)の分だけアイテムの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の取り出し

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

onDrawへ区切り線を描画

アニメーションに追従

アイテムのポジションが変わるような変更を行うと、変更の様子がアニメーションで表現されます。

区切り線がアニメーションへ追従するように、アイテムの位置変化量(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のアイテムが消える時に区切り線が同時に消えている。
スポンサーリンク

関連記事:

RecyclerViewは子Viewを並べて表示するコンテナタイプ(ConstraintLayoutと同じ)のViewです。 複数のデータをスクリーン上に一覧表示したい時、例えば電話帳のような「氏名+住所+電話番号」の一覧を表示する場合などに最適です。 アプリを開発していると一覧表示したいデータが多いことに気付きます。 なのでRecyclerViewはとても重要で重宝するViewです。 しかし、思い通りの表示を行わせるためのテクニックが多すぎて、使いこなしが難しいです。 今まで調べたテクニックを忘れないように、整理して書き残そうと思います。 今回は基本の「RecyclerViewの実装」です。 ...
RecyclerViewでアイテムのクリックイベントを取得し、処理を実行する方法を紹介します。 ...
RecyclerViewはアイテムのレイアウトをアイテム毎に変更できます。その時に使う値がViewTypeです。 ViewTypeでアイテムのレイアウトを変更する方法を紹介します。 ...
RecyclerViewは表示が変更される(アイテムの更新、スクロール)時、アイテムのViewをリサイクル(再生利用)します。 これにより余分なViewの作成が行われなくなり、メモリーの節約とパフォーマンスの向上が望めます。 リサイクルはCachedViewsとRecyclerPoolという2つのキャッシュで行われます。 このキャッシュを使ったリサイクルの動作を調べたので紹介します。 ...
RecyclerViewのリサイクル動作で使われるキャッシュは、サイズを大きくすれば多くのViewHolderが保持できます。その分、多くのメモリを消費します。 ViewHolderを多く保持できたとしても、サイクル動作で効率よく使われなければ、メモリの浪費です。 キャッシュのサイズはRecyclerViewの使われ方よって適切なサイズがあります。 そのため、RecyclerViewはキャッシュのサイズを変更できるようになっています。 ...
RecyclerViewはアイテムを一覧表示してくれます。 ただ一覧表示するだけではなく、「追加・削除・移動・切り替え」といったアイテムの表示を効率よく変更する仕組み持っています。 今回はこの仕組みを使ったアイテムの変更方法を紹介します。 ...
RecyclerViewのリサイクル動作で使われるキャッシュは、サイズを大きくすれば多くのViewHolderが保持できます。その分、多くのメモリを消費します。 ViewHolderを多く保持できたとしても、サイクル動作で効率よく使われなければ、メモリの浪費です。 キャッシュのサイズはRecyclerViewの使われ方よって適切なサイズがあります。 そのため、RecyclerViewはキャッシュのサイズを変更できるようになっています。 ...
RecyclerViewが空(アイテムが無い)の時、EmptyViewを表示する実装を行ったので紹介します。 ...
RecyclerView上のアイテムを選択する方法を紹介します。 外部ライブラリー(AndroidX)で提供されるrecyclerview-selection APIを用いた方法です。 ...
RecyclerViewはListView(RecyclerViewの前身)の時に存在していたChoiceModeがありません。 同様な機能が欲しければプログラマ側で実装しなければなりません。 RecyclerView.AdapterをカスタマイズしてChoiceModeを実装してみたので紹介します。 ...
RecyclerViewに表示しきれなかったアイテムはスクロールを行うことで表示されるようになっています。 スクロールは「外部入力(指でスクリーン上をタッチしてスライド)によるスクロール」の他に、「プログラムによるスクロール」をすることも出来ます。 今回はこのアイテムのスクロールについてまとめてみました。 ...
RecyclerViewはアイテムをスクロールさせて隠れたアイテムを表示できます。 スクロールを止める位置は任意です。 任意であるがゆえに、止めた位置によってはアイテムの一部が欠けてしまうこともあります。 携帯端末の画面は狭いので、効率よくコンテンツの表示を行いたいとアプリ開発者は考えます。 アイテムの一部が欠けてしまうことは、効率が良いとは言えません。 このような問題を解決するために、アイテムのスナップをRecyclerViewへ追加できます。 アイテムのスナップを追加する方法を紹介します。 ...
RecyclerViewでアイテムの変更(Change/Insert/Move/Remove)を行うと、変更される様子がアニメーション化されています。 これはデフォルトでアイテムの変更アニメーションが組み込まれているためです。 デフォルトは単純なアニメーションですが、「ある」と「ない」の違いは歴然で、アニメーションのある方が高価なアプリケーションに見えます。 GUI(Graphical User Interface)が主体の携帯端末にとって、利用者に対するアプリの見せ方は重要です。高価に見えた方が使ってもらえる可能性が高くなります。 上記のことから、アプリの機能に関係なくても、ちょっとした動きをアニメーション化するメリットがあります。 2回にわたりアイテムの変更アニメーションについてまとめてみました。 アイテムの変更アニメーション(DefaultItemAnimator、デフォルト) アイテムの変更アニメーション(SimpleItemAnimatorの継承、カスタム) 今回は第1回目「ItemAnimator、デフォルト」編です。デフォルト変更アニメーションの動作について説明します。 ...
RecyclerViewでアイテムの変更(Change/Insert/Move/Remove)を行うと、変更される様子がアニメーション化されています。 これはデフォルトでアイテムの変更アニメーションが組み込まれているためです。 デフォルトは単純なアニメーションですが、「ある」と「ない」の違いは歴然で、アニメーションのある方が高価なアプリケーションに見えます。 GUI(Graphical User Interface)が主体の携帯端末にとって、利用者に対するアプリの見せ方は重要です。高価に見えた方が使ってもらえる可能性が高くなります。 上記のことから、アプリの機能に関係なくても、ちょっとした動きをアニメーション化するメリットがあります。 2回にわたりアイテムの変更アニメーションについてまとめてみました。 アイテムの変更アニメーション(DefaultItemAnimator、デフォルト) アイテムの変更アニメーション(SimpleItemAnimatorの継承、カスタム) 今回は第2回目「SimpleItemAnimatorの継承、カスタム」編です。カスタム変更アニメーションの作り方を説明 ...
RecyclerViewへアイテムが表示されるとき、アニメーションはありません。。一瞬で表示されて終わりです。 「RecyclerViewへアイテムが表示される」ことを、ここでは「アイテムの出現」と言い表すことにします。 このアイテムの出現にアニメーションを付ける方法を紹介します。 アイテムの出現をアニメーションで演出することで、RecyclerViewに表示したい内容が際立つと思います。 ...
スポンサーリンク