メモ2ブログ

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

AlpineベースのAndroidビルド環境のDockerイメージを作成した

なるべく小さく、sdkmanagerにも対応させた、CI用のDockerイメージを作成しました。 Emulatorなどを利用したAndroid Testにはまだ未対応ですが、Unit Testは実行できます。

PublicなDockerHubに上げたかったのですが、ライセンス違反という話があるのでPrivateにしてあります。

github.com

↑なぜか画像がアイコンになってくれない。。

詰まったところ

sdkmanagerの実行で失敗

bashも入ってないため、sdkmanagerの実行で失敗します。

sdkmanager: line 159: syntax error: unexpected "(" (expecting "}")
apk add bash

が必要でした。

glibcがない

純粋なAlpineのイメージだと、glibcが入ってないためapptでコケました。

Wikiにインストール方法が載っていたのですが、うまく入れられませんでした。

調べたら、こちら が既にAndroidを動かせる状態であったのですが、glibcだけを入れたイメージがあった ので、そちらをベースに作成することにしました。

内部的にはどちらもこのリポジトリに依存してるみたいです。

sdkmanagerを利用すると怒られる

sdkmanagerを利用するとWarningが表示されました。

Warning: File ~/.android/repositories.cfg could not be loaded.

ファイルを作成することでWarningが消えました。

改善したいところ

SDKも含めると結局大きい

Dockerイメージ自体を置いておけたり、キャッシュを効かせられないと、時間がかかります。 しかし、今まで利用してたイメージのサイズが2GB程度だったのに比べて、今回作成したイメージは1GBを切った(DockerHubのtagの数字)ので、そういう意味では小さくできました。

ライセンス周りの承認をいい感じにしたい

現状は承認ファイルのようなものを固定で入れてます。

しかし、 SDK Toolsの25.3.0から sdkmanager で承認ファイルを管理できます。

$ sdkmanager --licenses
5 of 5 SDK package licenses not accepted.
Review licenses that have not been accepted (y/N)?

あとは y を数回押すだけで、 $ANDROID_HOME/licenses/ に、承認ファイルが作成されます。

次のようにすれば自動で全部Acceptできるはずです。

$ (while sleep 1; do echo "y"; done) | sdkmanager --licenses

DLできるSDK Toolsが更新されたときに合わせてDockerfileも更新しようと思っています。

作成してみて

なんとなく触ってたDockerも、なるべくレイヤーを少なくすることだったり、registryにpushすることなどができて理解が深まりました。

ただ、ベストプラクティスのようなものがわかっていないので今後利用していきながら修正してければなと思います。

参考

Alpine Linux入門 -内部構造とapkでパッケージインストール編- / tehepero note(・ω<)

お前のDockerイメージはまだ重い💢💢💢 / Speaker Deck

公開用DockerイメージにAndroid SDKを含めるのはライセンス違反という話 / Islands in the byte stream

Alpine Linux / Alpine Linux

Glibc / OSS 用語集

Running glibc programs / Alpine Linux Wiki

alvr/alpine-android / GitHub

frol/docker-alpine-glibc / GitHub

sgerrand/alpine-pkg-glibc / GitHub

Installing Android Studio in Ubuntu 14.04 64-Bit .android/repositories.cfg could not be loaded / Stack Overflow

SDK Tools Release Notes / Android Studio

android - Automatically accept all SDK licences / Stack Overflow

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