メモ2ブログ

メモtoウェブログ。旧ブログはこちら。 http://sakebook.blogspot.jp/

RecyclerViewで複数のDividerを実装するライブラリ「MultiLineDivider」を公開しました

以前の記事で、RecyclerViewで複数のDivider実装する方法をまとめたのですが、これを使うには ItemDecoration を実装したクラスを作る必要がありました。

そのあたりをよしなにして導入できるライブラリを公開しました。

github.com

使い方

DividerItemDecoration と同じように RecyclerView に追加します。

あとは ViewHolderVerticalDivider, HorizontalDivider, NoDivider を任意で実装するだけです。

使用上の注意

Kotlinで書いたので、Javaのみのプロジェクトだと dependencies に追記が必要です。

compile "org.jetbrains.kotlin:kotlin-stdlib:1.0.0"

作成上の小話

過去に作成したライブラリは、SonatypeからMaven Centralに登録してたのですが、Android StudioリポジトリのデフォルトがJCenterに変わっていたので、JFrog Bintry経由で登録しました。

参考

RecyclerViewで複数のDividerを実装する / メモ2ブログ

Building your own Android library::codepath/android_guides Wiki / GitHub

How to distribute your own Android library through jCenter and Maven Central from Android Studio / The Cheese Factory’s Blog

Bintray – Your Universal Distribution Platform / JFrog Bintry

How to use Maven Local repository for gradle build / Gradle Forums

Kotlin android library mavenAndroidJavadocs task fail::novoda/bintray-release / GitHub

RecyclerViewで複数のDividerを実装する

RecyclerViewでは、Divider(区切り)は自分で実装しなければなりません。

標準の線を引く方法はよく見つかるのですが、複数の線を引いたり、逆に線を引かなかったりするというのがあまり見つからないのでまとめました。

f:id:sakebook:20170423181700g:plain

Support Libraryに追加されたDividerItemDecoration

確認したSupport Libraryのバージョンは 25.3.1 です。

標準の線は、Support Libraryの DividerItemDecoration を使うことで引けます。内部の実装は、 mDivider というDrawableを、Canvasに描画しています。 なので、描画をさせなければ線を引かない風にできますし、描画するDrawableを変えれば、異なる線を引くことができます。

基本的な流れ

描画領域の確保

区切り線の描画領域は ItemDecoration#getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) で、セルのxmlで定義したセルの大きさから、さらに余白をつけることができます。

なので、区切り線を描画させたくないときは、 outRect.set(0, 0, 0, 0)すべて0にします。 下に引きたいときは、 outRect.set(0, 0, 0, mDivider.intrinsicHeight) と下に余白を取ります(left, top, right, bottom)。

mDivider.intrinsicHeight はDrawableの高さです。

セルごとの描画領域を求める

描画領域を確保したら、実際に描画します。

ItemDecoration#onDraw(Canvas c, RecyclerView parent, State state) で描画していきます。画面を動かす度に呼ばれます。

Support LibraryのDividerItemDecoration では、 drawVertical を定義してそこで横の区切り線の処理をしています。

Math.round(ViewCompat.getTranslationY(child) でセルの上部のY座標の求め、余白を加味したセルの高さを合わせて、区切り線を描画すべきY座標を求めます。

求めたY座標から、区切り線の高さを引くことで、区切り線を描画すべきRectがわかります。 求めたRectを、 mDivider にセットし、Canvasに描画することで指定した領域に区切り線が表示されます。

  • DividerItemDecoration.java(Support Library)
...
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
...

独自のDividerItemDecoration

Support Libraryの DividerItemDecoration では、標準の区切り線か、 DividerItemDecoration#setDrawable(drawable: Drawable) で設定した区切り線の1種類しか使えません。なので、 RecyclerView.ItemDecoration を継承して独自の DividerItemDecoration を作ります。

基本的な流れはSupport Libraryの DividerItemDecoration と同じなのでソースを丸ごと拝借します。

必要なことは、 そのセルに区切り線が必要かどうか と考えます。 セルから区切り線を必要としているかどうかをわかるようにします。

RecyclerView#getChildViewHolder(View child) で、セルのViewからViewHolderを取得できるので、ViewHolderに区切り線の有無の情報をもたせることにします。

特定のViewHolderに依存させない

Divider というInterfaceを定義します。さらに、それを実装した NoDividerCustomDivider を定義します。 CustomDivider には区切り線の高さと、実際に描画するDrawableのリソースを持たせます。

このInterfaceをViewHolderが実装することで、 DividerItemDecoration が特定のViewHolderに依存することを避けます。

  • getItemOffsets
...
val vh = parent.getChildViewHolder(view)
when(vh) {
    is NoDivider -> outRect.set(0, 0, 0, 0)
    is CustomDivider -> outRect.set(0, 0, 0, vh.height)
    else -> outRect.set(0, 0, 0, mDivider.intrinsicHeight)
}
...

Interfaceを実装していないViewHolderは、標準の区切り線を利用しています。

Drawableを使いまわす

CustomDivider のリソースからDrawableを都度作成すると、パフォーマンスが悪くなるのでDividerItemDecoration に保持させます。

...
private val mDividerMap: HashMap<Divider, Drawable> = HashMap()
...

一度作成した Drawableは保持して保持して、使いまわすようにします。

  • drawVertical
...
val vh = parent.getChildViewHolder(child)
when(vh) {
    is NoDivider -> {}
    is CustomDivider -> {
        val drawable = mDividerMap[vh]?: // Reuse divider
                ResourcesCompat.getDrawable(context.resources, vh.drawableRes, null)?.let {
                    mDividerMap.put(vh, it)
                }
        val top = bottom - (vh.height + 1) // Line height < Bounds height
        drawable?.setBounds(left, top, right, bottom)
        drawable?.draw(canvas)
    }
    else -> {
        val top = bottom -mDivider.intrinsicHeight
        mDivider.setBounds(left, top, right, bottom)
        mDivider.draw(canvas)
    }
}
...

区切り線の描画範囲が、Drawableより小さい場合は正しく描画されません。そのため、

val top = bottom - (vh.height + 1) として区切り線の開始位置を調整しています。これは bottom を求める際に Math.round(ViewCompat.getTranslationY(child) を利用しているのと、リソースから高さを求めているので、そのあたりの丸め誤差によって生じているのだと考えています。

ViewHolderに実装する

DividerItemDecoration を以上のように実装できれば、区切り線をつけたり消したりするのは ViewHolderを実装したクラスに、さらにDividerを実装するだけで済むようになります。

  • OddViewHolder.kt
class OddViewHolder(view: View): ViewHolder<Data>(view), CustomDivider {

    override val height = view.context.resources.getDimensionPixelSize(R.dimen.small_margin)
    override val drawableRes = R.drawable.dashed_line_divider
    ...
}
  • EvenViewHolder.kt
class EvenViewHolder(view: View): ViewHolder<Data>(view), NoDivider {
    ...
}

まとめ

一度作ってしまえば使いまわせるので便利です。 サンプルは横方向には対応していませんが、同じようにすることで対応できると思います。

ただし、ガイドライン的には、Dividerを多様をすることで本来の効果が薄れてしまう可能性があるとあるので使い所には注意が必要です。

サンプルはこちらです

github.com

参考

DividerItemDecoration / Android Developers

RecyclerView / Android Developer

RecyclerView.ItemDecorationについて #関モバ / Takuji->find;

How do I make a dotted/dashed line in Android? / Stack Overflow

How can a divider line be added in an Android RecyclerView? / Stack Overflow

ハードウェアアクセラレーションが有効だとstrokeでの点線描写が上手く描写出来ない / みんからきりまで

Dividers / Material design guidelines

DroidKaigi 2017で登壇してChrome Custom Tabsについて発表しました。

スライドとサンプルアプリと、そのコードです。

speakerdeck.com

play.google.com

github.com

スピーカーから見たDroidKaigiと、それにまつわる個人的なメモです。

スピーカーから見たDroidKaigi

DroidKaigiスケジュール

日程 出来事 行動
2016.10.01 CFP募集開始
2016.11.05 CFP応募
2016.11.15 採択内定 内定承諾、希望発表時間帯、前日のレセプションへの参加、個別の依頼事項、質問要望等返信
2017.01.10 タイムテーブル確定 オフィスアワーの可否、Tシャツサイズ選択
2017.02.13 会場設備について共有 プロフィール画像のアップロード
2017.02.22 スピーカーズディナー(前日のレセプション)案内
2017.03.07 当日の会場時刻と設備の案内
2017.03.09 1日目
2017.03.10 2日目 登壇

やり取りのメールを眺めると、上のような感じです。基本スピーカーは、届いたメールやアンケートに返信すれば良いだけになってます。

応募して採択されたけどやっぱり怖い!という人のために、一応内定を断ることもできます。

やることは少ないのですが、自分は画像のアップロードを忘れてしまっていて、デフォルト画像での参加となりました。

f:id:sakebook:20170312214749j:plain

DroidKaigi 前日

スピーカーズディナーに参加して、贈り物みたいなやつ(言葉でないw)を受け取りました。 居酒屋かな?と思っていたら、立食で、思った以上に人がいて驚きました。

DroidKaigi 1日目

受付が、専用受付がありスムーズでした。

登壇当日の動き

登壇当日は一つ前のセッションから、控室で資料の最終確認を行っていました。

めっちゃ緊張してたので、控室で緊張しない方法を尋ねると、色々温かいレスポンスがもらえたのが嬉しかったです。


20分の移動時間が始まり、部屋に移動しました。 早くから部屋に来てた方にChrome Custom Tabsを使ったことがあるか聞いたら、殆どの方が使ったことがないみたいでした。

事前にサンプルアプリを作ってGooglePlayに公開していました。 発表前にQRコードをスライドに写しておくことで、わりとスムーズにアプリの共有はできたのかなと思います。

発表前は、スタッフの方とタイトルの確認とオフィスアワー有無の確認をしました。


発表後はオフィスアワーとして20分程度バリスタさんのコーヒーを飲んでました。

個人的なメモ

アイコン大事

スピーカーズディナーとアフターパーティーで思ったのが、アイコン大事ってことです。

「あぁ!あのアイコンの人だ!」って思うことがよくあったので、忘れた自分は少し機会損失があったのかもしれないなと思いました。

事前知識の共有

1日目は、純粋に発表を聞くのと、スライドの作りや構成で参考にできないかなという目線で見てました。

自分も聞いててあったのですが、わからない部分が出てきたとき、それが今後の前提となってくると、ずっと置いてけぼりをくらってだんだんわからなくなる負の連鎖があります。

回避方法として、要所要所にまとめを設けるのと、事前知識として共有しておかないとまずいことは、時間を割いてでも共有しておくことで防げるのかなと思いました。

主観ですが、これができている発表は聞きやすかったように思います。

しかし、自分の発表には適応できませんでした。 Chrome Custom Tabsに触ったことのない方が多かったので、最低限のコードは入れておくと、後のイメージがしやすくなったんだろうなと思いました。

人に見てもらう

若干早いという指摘もありました(実際20分くらいで終わってしまった)。

リハでも早く終わってたので、質疑応答に充てようと思っていたのですが、実際は、前述のような追加できる要素が残っていました。

社内の人や、同じスピーカーの方に見てもらうなどできれば気づけた部分だったのかなと思いました。

スピーカー同士で、スライドの確認をできる場が公式であっても良いのかなとちょっと思いました。

やってよかった

お金を払って聞きに来てくださった方に、聞いてよかったという価値を提供することができたんだろうか?と不安だったのですが、発表後にも、オフィスアワーにも、アフターパーティーでも質問をしてもらったので、ある程度価値を提供できたのかなと思いました。

何も起きなかった運営すごい

前述の通り、スピーカーは発表以外にやることがほとんどありません。負担がないし、参加者としても問題なく楽しく参加できました。

当たり前のようになってますが、有志でこれだけ安定してイベントを運営するのは本当にすごいと思います。

まとめ

大きなイベントの登壇は不安ですが、大丈夫です。

来年も何か見つけてCFP応募するか、スタッフとしてお手伝いきればよいなと思いました。 両方してる人たちすごい。。

参考

DroidKaigi 2017 / DroidKaigi