Androidプロジェクトをシュッと開く
Androidプロジェクトを開く時、Android Studioのメニューから開くのはとてもめんどうくさい。File -> Open...
と選択してからプロジェクトの在り処を選択する必要がある。めんどうだ。マウス操作めんどう。
実はコマンドラインからプロジェクトをAndroid Studioで開くコマンドラインツールがAndroid Studioに備わっている。なんかしらのプロジェクトを開いている状態でTools -> Create Command-line Launcher...
を選んでコマンドを作るだけだ。
コマンドが作られると、プロジェクトがカレントディレクトリの状態で次のコマンドを実行するだけでプロジェクトを開くことができる。
studio .
楽ちん〜。
この機能はすべてのJetBrains系のIDEについているみたいなので、例えばRubyMineとかでも使えます。
さらにAlfredを使っている人は次のworkflowもオススメ
studio
と入力すると開くプロジェクトがでてくるので素早くプロジェクトを開くことができる。
ここにでてくるプロジェクトはAndroid Studioで開いたことがあるものだけなので注意が必要。
adb-pecoを書き直しました
複数デバイスが接続されていたときにデバイスを選択する支援をしてくれる adb-peco ですが、この度、Rubyでコードを書き直しました。
通常だとmore than one device/emulator
と出力されるようなシチュエーションでも、こんな風にコマンド実行時にデバイス選択することができます。
書き直したことにより次の2つの改善がなされました。
インストールの簡素化
Ruby Gemで作っているためインストールがコマンド1つで完結するようになりました。
gem install adb-peco
デバイス指定の必要のないコマンドではデバイス指定なしに
以前のshell版ではdevices
やkill-server
などでもデバイス指定が必要でした。これらのコマンドでは本来デバイス指定は必要ないので、なくしました。ホワイトリスト形式で除外しているので、デバイス指定が必要ないコマンドが他にありましたらissue立てて貰えると対応します。
最後に
Androidアプリのビルドが終わったらにゃーんと猫の声が聞こえるようにする
Androidアプリのビルドはターミナル派でしょうか、それともAndroid Studio派でしょうか。 最近はInstant Runや、接続されているデバイスの情報から不要なリソースを削ることでビルドの高速化されるなど、Android Studioでのビルドは魅力的なところが多いのですが、私は依然ターミナル派です。*1
いままで作ったコマンド
これまで、ターミナルで快適に開発ができるように、便利なコマンドを開発しては公開してきました。
techlife.cookpad.com techlife.cookpad.com
中でも特に気に入ってるものの1つがnotifier-plugin
です。
notifier-pluginとは
Androidアプリのビルドって結構長いですよね。規模によってはフルビルドで3~4分かかってしまうアプリも少なくないんじゃないでしょうか。
ビルドの待ち時間が長いとついついTwitterでハイハイワロスワロスと糞ツイートが捗ってしまいます。いつのまにかTwitterに熱中してしまってビルドが終わっても気づかずに10分以上Twitterやってたみたいな経験はないでしょうか。
そこで開発したのがnotifier-plugin
で、ビルドが終わったタイミングで音声で教えてくれたり、通知センターで教えてくれたり、決まった音楽を流すことができます。
導入方法はbuild.gradleに書いて、設定ファイルを1つ追加するだけです。
iTerm2
話は変わりますが、私は快適なターミナル生活のためにiTerm2を使っています。
iTerm2はMacのターミナルをもっと便利にするアプリケーションで、決まったショートカットを押すとシュッとターミナルを出すことができるところが気に入ってます。そのiTerm2が最近version 3になりました。ただシュッとするためだけに使ってきたiTerm2ですが、これを機に機能を調べているとTriggerという機能があり、これを使えばプラグインすらいらねーんじゃねと思って使ってみました。
iTerm2でビルドがにゃーんと分かる
Triggerの設定をしましょう。
Preferences > Profiles > $YOUR_PROFILE > Advanced > Triggers > Edit
+ボタンから新しいトリガーを設定します。
正規表現で一致したテキストが出力されると何らかのアクションを起こすことができます。
Gradleビルド成功/失敗時のログは次のようになります。
# 成功時 BUILD SUCCESSFUL # 失敗時 BUILD FAILED
よって正規表現を次のように定義しました。
^(BUILD (SUCCESSFUL|FAILED))$
起こすアクションはコマンドの実行です。 次のコマンドを実行するように定義しました。
say -v 'Alex' \1
Macにプリインされているsay
コマンドを使い、Alexがビルド結果を教えてくれます。
正規表現で一致したテキストをアクションで使用しています。そのへんのルールはここに https://www.iterm2.com/documentation-triggers.html あります。
にゃーんと鳴いてねーじゃねーかと思いますが、任意の音声を再生するならこんな感じでしょうか。
afplay ~/Downloads/meow.mp3
これでにゃーんと鳴きます。
通知センターで表示しながらAlexに喋らせるならこんな感じでしょうか。
echo 'display notification "\1" with title "Gradle"' | osascript; say -v 'Alex' \1
まとめ
Gradle pluginを使わずにビルドが終わったことを音などで知らせることができました。これはAndroidアプリのビルド以外にも時間がかかる処理に応用ができてなかなか便利ですね。
またnotifier-plugin
はGradle pluginとして動作するので、Android Studio経由のビルドでも音を鳴らすことができるので、そちらもぜひご活用ください。
*1:Instant Runがまだ不安定な部分があり、完全に導入しきれてないなどの理由がありますが、多くは語りません
Androidエンジニアは自分専用のDockerイメージを作るべきだと思った
初めてDockerイメージを作ったのでエントリに書いておきます。タイトルはすこし言い過ぎな感じはありますが、CIにDocker使っている人は自分でDockerイメージ作った方がいいよねって話です。
私とCI
CIという言葉もかなり広まり、会社で使うだけではなく個人の開発でもCIを導入している人は多いのではないでしょうか。私はVPN + JenkinsでCIを始めて、1年半前にWerckerに乗り換えました。Jenkinsにはとてつもない自由さやCIがコケた時に原因究明が楽な点がありますが、メンテナンスが大変でした。CIサービスはメンテナンスがあまり必要なくてとても気に入ってます。
Werckerのメリットについては他に書かれている方がたくさんいるので、適当にググって読んでみてください。
WerckerとDocker
私がWerckerに移行した当時はBoxと呼ばれる実行環境でビルドするようになっていましたが、少し前からDockerに差し替わりました。Dockerというキーワードは知っていたもののイメージを作ったことも使ったこともなかったので、適当にイメージを探して他の人のイメージを使わせてもらっていました。しばらくはそれで大丈夫だったのですが、最近この辺でストレスが溜まるようになってきました。
ストレスの原因
一番の理由はAndroidのbuild toolやサポートライブラリのアップデートについていくために実行環境のアップデートをしないといけないのですが、他人のイメージを使っているがために、なかなかアップデートすることができないところです。Dockerfileはオープンソースで公開されているんだからPR出せばいいじゃんという声も聞こえてきそうですが、PRを出したとしてもすぐにマージされるわけではないので、そこでストレスが溜まってしまいます。他の人のライブラリや環境を使うなら当然のリスクですが、プライベートで開発できる時間は限られています。こういった待ち時間があるとついついモチベーションが落ちがちなのでどうにかしたいです。
環境の変化
Android誕生してからもう少しで10年経とうとしていますが、その開発環境は決して枯れることはなく常に変化しています。今もJack and JillやOpen JDK移行などが迫ってきており、今後も開発環境の決定版といえるものはでてこないと思います。
使いたいパッケージの違い
また、人によって使いたいパッケージは異なります。私の場合、CIが終わった後にSlackで通知をしたく、そういったプラグインを使っているのですが、そのプラグインがRubyで書かれているためRubyをインストールする必要があります。こういった要望はなかなかPRにすることはできません。
そして自分のDockerイメージを作った
というわけでこのストレスを解消すべく自分でAndroidアプリのビルドができるDockerイメージを作りました。
作ったイメージはdocker hubに公開したので、試してみたいという方はどうぞ
https://hub.docker.com/r/tomorrowkey/wercker-container-android/
DockerfileもGitHubに公開しました。
できるだけ最新のものにアップデートしていきますしそういったPRも大歓迎ですが、ベストエフォートなのであしからず。そこでストレスが溜まってきたら自分のイメージを作るいいタイミングだと思います。私のリポジトリをforkしてもいいですし、一から作るのもDockerを理解することができると思うのでおすすめです。
メモ
以下、イメージを作るにあたってどう解決したかメモを残しておきます。
OS
FROM ubuntu:14.04
ベースはAndroidのビルド環境によく使われるUbuntuにした。
Oracle Java8のインストール
# Java8 installation RUN \ apt-get install -y software-properties-common curl && \ add-apt-repository -y ppa:webupd8team/java && \ apt-get update -y && \ echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \ apt-get install -y oracle-java8-installer
oracle-javaのインストールはapt-getに新しくリポジトリを追加することでできると知った。
How To Install Oracle Java 8 In Debian Via Repository [JDK8] ~ Web Upd8: Ubuntu / Linux blog
リポジトリ追加したらapt-get update
してインストール可能なソフトウェアの更新が必要なところに注意
echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections
と書かれているところは使用許諾の同意に必要。
Docker上でOracle Javaの実行はライセンス違反の可能性があるとの情報を得たのでOpen JDKを使うように修正しました。
Dockerコンテナ上でのJavaの実行はライセンス違反なのか?
RUN \ apt-get install -y software-properties-common curl && \ add-apt-repository -y ppa:openjdk-r/ppa && \ apt-get update && \ apt-get install -y openjdk-8-jdk
OpenJDK8もppaの追加が必要なので、add-apt-repository
を使ってインストール。以前OpenJDKを使ったときにビルドできないなどの不具合があった気がしたが、いまのところ問題なさそうなのでしばらくこれでやってみようと思います。
Android SDKのインストール
# Android SDK Installation ENV ANDROID_SDK_REVISION r24.4.1 RUN \ cd /usr/local && \ curl -L -O "https://dl.google.com/android/android-sdk_$ANDROID_SDK_REVISION-linux.tgz" && \ tar -xf "android-sdk_$ANDROID_SDK_REVISION-linux.tgz" && \ rm "/usr/local/android-sdk_$ANDROID_SDK_REVISION-linux.tgz" RUN apt-get install -y lib32z1 lib32gcc1
Android SDKのインストールはアーカイブをダウンロードして、配置すればよいと思っていたが、aaptが使うパッケージが足りなかったみたいなので、追加でlib32z1
とlib32gcc1
をインストールしている。実際にCIでビルドしようと思っていたアプリをビルドしてみたらaaptがエラーを吐いていたので何か足りないんだなと分かった。あとは適当に調べてパッケージをインストールした。
androidコマンドがなくなる
RUN echo y | android update sdk --no-ui --force --all --filter "tools" RUN echo y | android update sdk --no-ui --force --all --filter "platform-tools"
toolsとplatform-toolsを同時にインストールしようとしたが、そしたらandroidコマンドがなくなるという不具合に出会った。別にインストールすると問題なかった。
Rubyのインストール
# Ruby installation RUN add-apt-repository -y ppa:brightbox/ruby-ng && apt-get update && apt-get install ruby
ビルド終わった後にSlackで通知するようにしているのだが、そのプラグインにRubyが必要なのでインストールしている。
当初はrbenvを使ってRubyをインストールするように実装してみたが、rbenvはexport
で環境変数を設定しようとする。Dockerではビルド中にexport
しても無視されてしまうので、素直にrbenvを使うことができない。ではDockerで環境変数を設定するにはどうするかというとENV
を使わなければならない。どうにかスクリプトに書き込むことでrbenvを使うことができるようになったが、CI上ではパスが通らなくて調査困難で諦めた。
Rubyのインストールも新しくリポジトリを追加しないといけなかったので、追加してる。
Ruby packages for Ubuntu - Brightbox Cloud
apt-add-repository vs add-apt-repository
リポジトリの追加するコマンドはapt-add-repository
とadd-apt-repository
があるみたいで、どう違うか調べた。どちらも一緒らしいので、add-apt-repository
を使うように揃えた。
Dockerコマンドとか
docker-machineの起動
docker-machine start default
Dockerfileのビルド
docker build .
イメージの列挙
docker images -a
イメージの起動
docker run -it <IMAGE_ID>
コンテナの列挙
docker ps -a
コンテナの停止
docker rm <CONTAINER_ID>
全コンテナの停止
docker ps -aq | xargs docker rm
ネットワークが繋がらなくなる
ルノアールにてdocker buildしようとしたらネットワークが不通になることに少しだけハマった。詳しくはよくわからないが、接続するネットワークが変わったことが原因っぽい。docker-machine restart
で再起動すると治った。
RoundRectShapeの引数について調査した
角丸画像をコードから生成したくて、ShapeDrawableを使うことになった。 角丸にするにはRoundRectShapeをShapeDrawableに食わせるんだけど、その引数がドキュメントだけだと分からなかったので検証した。
RoundRectShape(float outerRadii, RectF inset, float innerRadii) RoundRectShape constructor. Specifies an outer (round)rect and an optional inner (round)rect. outerRadii float: An array of 8 radius values, for the outer roundrect. The first two floats are for the top-left corner (remaining pairs correspond clockwise). For no rounded corners on the outer rectangle, pass null.
時計回りで指定すればいいんだよってことは分かるけど、8つ渡すのはよく分からなかった。
RoundRectShape | Android Developers
結論からいうと以下のようになっている
float[]{
topLeft-x, topLeft-y,
topRight-x, topRight-y,
bottomRight-x, bottomRight-y,
bottomLeft-x, bottomLeft-y
}
4つの角 + xとyで8つある理由ですね。 検証に使用したコードを載せておきます。
import android.content.Context; import android.databinding.DataBindingUtil; import android.graphics.Color; import android.graphics.Paint; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RoundRectShape; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import jp.tomorrowkey.android.roundrectshapeapp.databinding.ActivityMainBinding; import jp.tomorrowkey.android.roundrectshapeapp.databinding.ListItemBinding; public class MainActivity extends AppCompatActivity { ActivityMainBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_main); binding.recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); binding.recyclerView.setAdapter(new MyRecyclerViewAdapter(this)); } private static class MyRecyclerViewAdapter extends RecyclerView.Adapter<ViewHolder> { private static float[][] PATTERNS = new float[][]{ {50, 50, 0, 0, 0, 0, 0, 0}, {0, 0, 50, 50, 0, 0, 0, 0}, {0, 0, 0, 0, 50, 50, 0, 0}, {0, 0, 0, 0, 0, 0, 50, 50}, {50, 50, 50, 50, 0, 0, 0, 0}, {25, 50, 25, 50, 0, 0, 0, 0}, {25, 50, 25, 50, 25, 50, 25, 50}, }; private static final int SOLID_COLOR = Color.rgb(0x00, 0x80, 0x80); private static final int RECT_SIZE = 100; LayoutInflater inflater; public MyRecyclerViewAdapter(Context context) { inflater = LayoutInflater.from(context); } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ViewHolder(inflater.inflate(R.layout.list_item, parent, false)); } @Override public void onBindViewHolder(ViewHolder holder, int position) { ListItemBinding binding = DataBindingUtil.bind(holder.itemView); float[] outerR = PATTERNS[position]; binding.textView.setText("{" + Util.join(outerR, " ,") + "}"); ShapeDrawable drawable = new ShapeDrawable(new RoundRectShape(outerR, null, null)); drawable.getPaint().setColor(SOLID_COLOR); drawable.getPaint().setStyle(Paint.Style.FILL); drawable.setIntrinsicHeight(RECT_SIZE); drawable.setIntrinsicWidth(RECT_SIZE); binding.imageView.setImageDrawable(drawable); } @Override public int getItemCount() { return PATTERNS.length; } } private static class ViewHolder extends RecyclerView.ViewHolder { public ViewHolder(View itemView) { super(itemView); } } }
ObjectAnimatorでTextViewの内容を書き換える
ObjectAnimatorおもろいなー setNumber(int)を持つNumberTextViewというクラスを作り
public class NumberTextView extends AppCompatTextView { public NumberTextView(Context context) { super(context); } public NumberTextView(Context context, AttributeSet attrs) { super(context, attrs); } public NumberTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public void setNumber(int number) { setText(String.valueOf(number)); } }
ObjectAnimatorでエイッとすると
ObjectAnimator objectAnimator = ObjectAnimator.ofInt(binding.numberTextView, "number", 0, 10000); objectAnimator.setDuration(10 * 1000); objectAnimator.setInterpolator(new LinearInterpolator()); objectAnimator.start();
数値がテキストとして表示される
単にリフレクション使って値を設定しているだけなのかなー。
ProgressBarの値をアニメーションで変更する
↑こういうことやりたいんだけど、ProgressBar#setProgress(:int)
だとアニメーションしてくれない。
そうだ、ObjectAnimatorを使ってアニメーションを実現しよう。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" tools:context=".activity.MainActivity" > <LinearLayout android:orientation="vertical" android:gravity="center" android:layout_width="match_parent" android:layout_height="match_parent"> <ProgressBar style="?android:attr/progressBarStyleHorizontal" android:id="@+id/progress_bar" android:layout_margin="16dp" android:layout_width="match_parent" android:layout_height="wrap_content"/> <Button android:id="@+id/update_progress_button" android:text="Update progress" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> </layout>
public class MainActivity extends AppCompatActivity { ActivityMainBinding binding; ObjectAnimator objectAnimator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_main); binding.updateProgressButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int progress = (int) (Math.random() * 100); setProgressWithAnimation(progress); } }); } private void setProgressWithAnimation(int progress) { int[] values = {binding.progressBar.getProgress(), progress}; if (objectAnimator == null) { objectAnimator = ObjectAnimator.ofInt(binding.progressBar, "progress", values); } else { objectAnimator.cancel(); objectAnimator.setIntValues(values); } objectAnimator.setDuration(1000); objectAnimator.setInterpolator(new DecelerateInterpolator(2.0f)); objectAnimator.start(); } }
Activityに書いちゃうとアレなんでカスタムViewでやるとスッキリしそうでよさそう。
アニメーションしている最中はそのときのProgressの値になってしまうので、getProgress()
を使いたいケースではこれは使えない。