AlpineベースのAndroidビルド環境のDockerイメージを作成した
なるべく小さく、sdkmanagerにも対応させた、CI用のDockerイメージを作成しました。 Emulatorなどを利用したAndroid Testにはまだ未対応ですが、Unit Testは実行できます。
PublicなDockerHubに上げたかったのですが、ライセンス違反という話があるのでPrivateにしてあります。
↑なぜか画像がアイコンになってくれない。。
詰まったところ
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
Running glibc programs / Alpine Linux Wiki
frol/docker-alpine-glibc / GitHub
sgerrand/alpine-pkg-glibc / GitHub
SDK Tools Release Notes / Android Studio
android - Automatically accept all SDK licences / Stack Overflow
RecyclerViewで複数のDividerを実装するライブラリ「MultiLineDivider」を公開しました
以前の記事で、RecyclerViewで複数のDivider実装する方法をまとめたのですが、これを使うには ItemDecoration
を実装したクラスを作る必要がありました。
そのあたりをよしなにして導入できるライブラリを公開しました。
使い方
DividerItemDecoration
と同じように RecyclerView
に追加します。
あとは ViewHolder
に VerticalDivider
, 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
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(区切り)は自分で実装しなければなりません。
標準の線を引く方法はよく見つかるのですが、複数の線を引いたり、逆に線を引かなかったりするというのがあまり見つからないのでまとめました。
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を定義します。さらに、それを実装した NoDivider
と CustomDivider
を定義します。
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
を多様をすることで本来の効果が薄れてしまう可能性があるとあるので使い所には注意が必要です。
サンプルはこちらです
参考
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