BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

RecyclerViewでGridLayoutの余白をうまく調整するItemDecoration

こんにちは。BASEでAndroidアプリ開発をしている鈴木です。
https://twitter.com/G_devi

突然ですが、デザイナーからこんな感じにしてくれと頼まれたことはありませんか?

f:id:gendevi01:20170329172157p:plain

GridLayoutですね

一番上のヘッダー部分が全幅なうえ、Grid部分の余白を全部同じサイズに調整するのが地味に手間がかかる。

どうせならヘッダーも同じサイズの余白をつけるか、Gridの左右は半分でもいいじゃん・・・と思うのは実装者だけですね。はい。

というわけで、楽に対応できるItemDecorationを作りました!


どうやってるのか

簡単に説明すると

  • RecyclerVIewの左右に設定したい余白の半分のpaddingをつける。
    • もう半分は子Viewで
  • RecyclerViewのclipToPaddingをfalseにする。
    • 子Viewにpadding領域はみ出してもいいよ設定
  • 全幅のViewの左右Marginに設定したい余白の半分の負数を設定する。
    • お言葉に甘えてはみ出すよ
  • GridItemの左右に設定したい余白の半分のpadding(DecorationでのoutRect)をつける。
    • GridItem間はお互いの余白を足して設定したい余白になる

中身

  • ItemDecoration
public GridDecoration(int sideMargin) {
    this.halfMargin = sideMargin / 2;
    this.halfMinusMargin = -1 * halfMargin;
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {

    int adapterPosition = parent.getChildAdapterPosition(view);
    GridLayoutManager lm = (GridLayoutManager) parent.getLayoutManager();
    GridLayoutManager.SpanSizeLookup ssl = lm.getSpanSizeLookup();
    int spanCount = lm.getSpanCount();

    if (ssl.getSpanSize(adapterPosition) >= spanCount) {
        // 全幅(ヘッダーとか)
        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
        params.setMargins(this.halfMinusMargin, params.topMargin, this.halfMinusMargin, sideMargin);
        view.setLayoutParams(params);
        return;
    }

    outRect.left = this.halfMargin;
    outRect.right = this.halfMargin;
}
  • RecyclerViewへの設定
recyclerView.setClipToPadding(false);
recyclerView.setPadding(
    halfMargin
    , parent.getPaddingTop()
    , halfMargin
    , parent.getPaddingBottom()
);
recyclerView.addItemDecoration(new GridDecoration(sideMargin));

こんな感じです

githubに置いたので他の部分も見たい人はぜひ
GridDecorationTest/GridDecoration.java at master · g-devi/GridDecorationTest · GitHub


失敗例

// 上で全幅は除外してる想定

// 左端のView
if (isLeft(ssl, spanCount, adapterPosition)) {
    outRect.left = sideMargin;
    outRect.right = sideMargin / 2;
    return;
}

// 右端のView
if (isRight(ssl, spanCount, adapterPosition)) {
    outRect.left = sideMargin / 2;
    outRect.right = sideMargin;
    return;
}

outRect.left = sideMargin / 2;
outRect.right = sideMargin / 2;

パッと見、うまくいってると思ったが・・・
よくこのGridのViewに横幅に合わせた正方形の画像を置いて、下にテキストを表示するようなレイアウトがありますよね?
それを当てはめてみると・・・

f:id:gendevi01:20170329173532p:plain

2行目の4つのViewを見てください。
オレンジの部分が横幅に合わせた正方形のViewなのですが、左右端と間の2つでサイズがズレてますよね?

これはoutRectを設定したことで左右端のViewの描画幅が狭まっているためです。

よく利用させていただいているこのアプリを使うと分かりやすいです。
play.google.com

spanCountとspanSizeを設定してる時点で幅は決まっており、その範囲内でどれだけ余白をもたせるかのoutRectですね。

ってことで、Gridの全ViewのoutRectのleft, right合計が一緒でないといけないためoutRectだけでの設定は物理的に無理でした・・・
やっぱりRecyclerView自体にpadding設定しなきゃってことで上の対応になったわけです。

補足

  • 上のコードだけだと、いわゆるdividerを入れていないため上下のViewがくっつくのでその辺はお好きに。
    • サンプルだととりあえず "outRect.bottom = sideMargin" を入れて表示してます。
  • とりあえずItemDecoratoinだけで設定してみたかったので、githubのコードではItemDecoration内でRecyclerViewのpaddingを設定したりしてますが、実際に使うときはRecyclerViewをカスタムするか、もう一個上でDecorationのセットも含めて実行してくれるものを作ったほうがいいかと思います。
  • レイアウトxml側でRecyclerViewのclipToPaddingやpaddingLeftなどを設定したり、全幅のものにマイナスマージンつけたりしてもいいのですが、作る度にそれぞれ設定すると手間なのでItemDecoration上でできるようにしました。
    • 手間でなければお好きなほうで!

最後に

  • そのうち、GridのViewがCardViewのパターンについても書きます。
    • 旧バージョンを考慮するとCardViewの周りにshadowのための余白がつくので調整が必要になりますからね・・・
    • zeplinとかでの指定ではshadowを無視したCardViewの端から画面端までの余白とかになりますからね・・・
  • BASEではAndroidエンジニアも募集しているので、気になる方はぜひ!!

www.wantedly.com