- 投稿日:2020-11-30T22:09:33+09:00
Android Studioおすすめプラグイン
はじめに
今回はAndroid Studioのおすすめプラグインを紹介します。
Save Actions
こちらは自動保存時に自動でコード整形やimportの最適化をしてくれます。
Android Studioのコード整形はcommand+option+Lなのですが、忘れることが多いです。
また使用しないimportを消すことはできません。そんな時にSave Actionsはまじで役立ちます。
特にチーム開発の時にこの人のこのインデント気に入らない、import消し忘れが治らないなどなどの悩みがなくなります。Nyan Progress Bar
今回はこれが紹介したかっただけです。
ビルド時やプロジェクトの読み込み時に猫が走ってくれます。
とにかく可愛いニャンニャンです。
- 投稿日:2020-11-30T20:45:07+09:00
GNSS(GPS)と「高さ」の話
はじめに
GNSS(GPS)を使った位置取得は、人類の大いなる進歩です!
我々はもう道に迷わないのです。(NEVER LOST AGAIN グーグルマップ誕生 なかなか面白い本でした。)
しかしながら、地図で自分の場所がわかって便利という以上のことを考えるためには、様々なこの分野特有の背景知識を必要とします。
本稿では、「高さ」というものに注目して、なんとなく常識だと思っていた概念の裏を見ていきましょう。「高さ」という概念について
皆さんは、高さという概念について、どのようにとらえていますか?
富士山は3776mだし、東京タワーは333mでスカイツリーは634mという有名な数字は出てくる人が多いでしょう。
富士山の数字は標高の数字で、東京タワーやスカイツリーは所在地の標高からの差分なので、若干富士山の数字とは基準が違うことになります。この基準というところが高さを考えるうえで重要になってきます。
ちなみに太陽系で最も高い山は火星のオリンポス山で、地表からの高さは実に約27000mだそうです。将来火星観光の目玉の登山になるかもしれませんね。
標高とは?
さて、改めて高さの表し方の一つ、「標高」の定義についてみていきましょう。
標高の定義
日本での標高の定義は、「東京湾の平均海水面からの高さ」です。海抜という言葉もあるように、海からの高さというのは、高さの概念の基本となります。
日本でのと書いたのは、実は国によって違う場合が多いからです。
東京湾という日本国内の基準をベースとするわけで、これが国際的に通用するものではないことは容易に想像がつきますが、
他の国も「近代的な」測地方法では、平均海水面を基準とすることが多いようです。東京湾の平均海水面の定義をさらに詳しく見ると、
平均海水面定義(Wikipediaより https://ja.wikipedia.org/wiki/日本水準原点)
霊岸島量水標(現在の東京都中央区新川、当時の隅田川河口にあたる。)における1873年6月から1879年12月までの毎日(一時期欠測あり)の満干潮位を測定して平均値を算出し、量水標の読み(荒川工事基準面、Arakawa Peil、A.P.)で1.1344メートルを東京湾平均(中等)海面 「T.P.」(Tokyo Peil)とし、この位置をゼロメートルとして全国の標高の基準と定めたとなります。この高さを基準にして、全国の標高を測っています。
標高の測り方
では、任意の地点の標高はどのように測るのでしょうか。
標高の測量、水準測量と言っていますが、近代的な測量手法では、絶対的なその地点の標高をその地点だけで測ることができません。そのため、基準点からの相対的な高さとして求めていきます。
三角点や水準点という形で日本全国に既知でかつ定期的に測量しなおされている基準点があり、そこからの相対的な高さとして求めます。
もちろん、それぞれの基準点も基準原点・水準原点からの相対的な位置・高さになります。水準測量は、以下の図のように水平をとれる水準器を中に置き、大きな定規のような標尺の目盛りを測ることで2点間の高さの相対値を測ります。
これを基準点から連続して行うことで任意地点の基準点からの相対的な高さを測るのです。そのため、標高の基準としてどこかに必ず0mの基準が必要なのと、どう頑張ってもそこからの相対値になります。
また、長い距離があると誤差や見通せないといった問題で測量ができません。
そのため、各国ごとに標高の基準は違いました。After GPSの世界
GPSの登場で、この状況に終止符が打たれます。これまで海水面という場所によって違う基準で測っていたものが、地球という基準をもとにして、世界中で統一された座標系で測ることができるようになりました。これ以降が今我々が享受している「現代的な」測量の世界です。
今、我々がGNSS(GPS)によって得ている座標は、世界のどこに行っても同じ基準で測られたものなのです。
素晴らしいですね。ちなみに、このGPSによる地球基準の座標系(測地系)としてWGS84というものが決められています。このWGS84の定義の中に、後述する楕円体の定義やジオイドモデルの定義も含まれています。この測地系という概念は、地理情報系の事を扱うときにとても重要なものなので、是非覚えて帰ってください。
(とはいえ、GNSS(GPS)は深宇宙では使えません。太陽系の中での探査機の位置決定には、VLBIという方法や、光学カメラから星の位置を調べて自己位置を決定するなどのやり方があるそうです。人類が太陽系を縦横無尽に旅するような時代になったら宇宙版GPSみたいなものができるのかもしれませんね。)
楕円体高
しかし、これまでの標高という概念とGNSS(GPS)が計測する高さには大きな違いがあります。
GNSS(GPS)が測る高さは、地球を回転楕円体としてみたときのその回転楕円体表面からの高さになります。
これを楕円体高と言っていますが、この高さは標高とは違うものです。ジオイド
そこで、標高と楕円体高を相互に関連付けるためにジオイドという概念が導入されます。
(ジオイド自体は地質学などで概念としてありましたが、衛星測位のためにより精密なモデルが作成されたそうです)
ジオイドの表面は地球の平均海水面に一致する等ジオポテンシャル面という定義なのですが、標高の基準となる平均海水面を仮想的に定義したようなものです。
このジオイド面の楕円体高からの高さがジオイド高で、地球全体でジオイド高を定義したものをジオイドモデルといいます。
そして、高さの計算は、以下の計算式で行えます。標高 = 楕円体高 - ジオイド高なお、ジオイドは重力と密接に関連した概念です。地球上では場所によって実は重力が微妙に違い、その違いを表しているのがジオイドという解釈もできます。水準測量のときは、水平器という液体に気泡が入った器具で水平を出すのですが、器具の仕組みとして重力的な水平を出していることになります。そのため、標高という数値は重力と紐ついているものなのでジオイドにより楕円体高から計算できている、というように自分では理解しています。
ジオイドモデル
よく地球の形を、洋ナシのような形ということがありますが、大体このジオイドモデルの形状を言っています。
単純な計算では表せない、非常に複雑な形をしています。
以下の図は、実際にEGM96のジオイドモデルをUnityで3D表示したものです。(高さ方向に引き伸ばして強調しています)このジオイドモデルですが、重力の観測や、水準測量の結果から作成されます。日本周辺の高精度なジオイドモデルを国土地理院が整備しています。GNSS(GPS)による座標測位を補強する重要な情報であり、様々な観測の基盤情報となります。
スマートフォンにおける高さの定義
さて、ここまでは事前知識の説明です。ここから本題。
ほぼすべてのスマートフォン端末にGNSS(GPS)が組み込まれるようになり、皆さん便利に使っていますが、実はAPIで取得できる情報には大きな違いがあります。iOSのCoreLocationでは、CLLocationで返ってくる高さはAltitudeつまり標高となります。
https://developer.apple.com/documentation/corelocation/cllocationAndroidでは、LocationのAltitudeの説明としてWGS84楕円体からの高さと定義が書いてあります。つまり楕円体高になります。
https://developer.android.com/reference/android/location/Location.html問題点
おそらくiOSの内部では、GNSS(GPS)で取得した楕円体高から内部に持ったジオイドモデルにより標高を計算しているのでしょう。
この違いを知らずに、両方で動くアプリケーションを作ると、場合によっては高さが数十mずれたりすることにもなります。
また、iOSの方は、生のセンサー値としての楕円体高を正確には取得できないことになります。
どちらにしろ、より正確な位置情報のマルチプラットフォーム運用をしたい場合には、ジオイド高の計算を自前で持つ必要がありそうです。ジオイド高の計算
ということで、ジオイド高を計算してみましょう。
今回は、アメリカ合衆国のNGA(国家地理空間情報局)とNASAが公開しているEGM96という少し古いジオイドモデルのデータと補間計算のプログラムを、Unityで使うことを前提にC#に移植してみました。もっとも、Unityの標準のGNSS(GPS)座標を取得する仕組みだと緯度経度がfloatで渡されて非常に残念なので、プラットフォーム側で位置情報取得してUnityに精度を維持して渡すのがよいでしょう。元にしたデータとプログラムはこちらです。
https://earth-info.nga.mil/GandG/wgs84/gravitymod/egm96/egm96.html元になるプログラム
上記ページのINTPT.FというFORTRANで書かれたプログラムを元にします。
FORTRANとか久々ですね。大学の時にやはり同じような分野のプログラムを解読して以来です。移植で苦労したのは、配列の添字の開始が1からだったところですね。FORTRANの常識として知ってはいたので注意深く書いたつもりだったんですが、いくつかバグを仕込みました。
また、数値の丸めをちゃんと指定しないと結果が同一にならないなどもありました。コメントを見ると、元のプログラムは1996年に作成されたもので、(EGM"96”なので1996年に制定されたモデルなのでプログラムも同時期に作成されたのでしょう)ベンチマークに載っていたCDCの3文字が時代を感じさせますね。今はなきControl Data Corporationのマシンでベンチマークされています。
こういう、過去のプログラムに関わるとコンピュータ考古学みたいで楽しいですね。
なお、近い時代のコンピュータ開発のドキュメンタリとして、「超マシン誕生」という本がありますね。Windows NTの開発を描いた「闘うプログラマー」と合わせて、コンピュータ業界を描いたドキュメンタリとして有名な1冊です。
C#での移植実装
以下が補間計算のメインプログラムです。コンストラクタでArrayHのインスタンスを指定して、interpメソッドに緯度経度を与えるとジオイド高が取得できます。
GeoidInterpolateEGM96.csusing System; using System.Collections.Generic; using System.Text; using System.IO; using System.Runtime.CompilerServices; using System.IO.MemoryMappedFiles; namespace GeoidInterpolateLib { public class GeoidInterpolateEGM96 { private const double south = -90.000000, north = 90.000000, west = 0.000000, east= 360.000000, dphi= 0.250000, dlam = 0.250000; private const int IWINDO = 4; public const int NBDR = 2 * IWINDO; public const int NLAT = 721, NLON = 1441 + NBDR; private const double DLAT = 0.25, DLON = 0.25; private const double PHIS = south, DLAW = west - NBDR / 2 * dlam; private const double RHO = 57.29577951; private const double REARTH = 6371000.0; private ArrayH H; private const int IPA1 = 20; private double[] A = new double[IPA1]; private double[] R = new double[IPA1]; private double[] Q = new double[IPA1]; private double[] HC = new double[IPA1]; public GeoidInterpolateEGM96(ArrayH AH) { H = AH; } public float interp(double lat,double lon) { double gh = interp(12.0, lat, lon); return (float)Math.Round(gh,3,MidpointRounding.AwayFromZero); } [MethodImpl(MethodImplOptions.Synchronized)] public double interp(double DMIN, double PHI, double DLA) { double ILIM = DMIN * 1000.0 * RHO / (REARTH * DLAT); double JLIM = DMIN * 1000.0 * RHO / (REARTH * DLON * Math.Cos((PHIS + DLAT * NLAT / 2.0) / RHO)); double RI = (PHI - PHIS) / DLAT; double RJ = (DLA - DLAW) / DLON; int I0, J0; I0 = (int)RI; J0 = (int)RJ; I0 = I0 - IWINDO / 2 + 1; J0 = J0 - IWINDO / 2 + 1; int II = I0 + IWINDO - 1; int JJ = J0 + IWINDO - 1; if (I0 < 0 || II >= NLAT || J0 < 0 || JJ >= NLON) { System.Console.Error.WriteLine("ERROR : PHI=" + PHI + " DLA=" + DLA + " STATION TOO NEAR GRID BOUNDARY - NO INT. POSSIBLE"); throw new Exception(); } else if (I0 < ILIM || II > NLAT - ILIM || J0 < JLIM || JJ > NLON - JLIM) { System.Console.Error.WriteLine("ERROR : PHI=" + PHI + " DLA=" + DLA + " STATION OUTSIDE ACCEPTABLE AREA - NO INT. PERFORMED"); throw new Exception(); } for(int i = 0;i < IWINDO; i++) { for(int j = 0;j < IWINDO; j++) { A[j] = H.GetH(I0 + i,J0 + j); } initspA(); HC[i] = splineA(RJ - J0 + 1.0); } initspHC(); return splineHC(RI - I0 + 1.0); } private static void printArray(double[] array) { for(int i = 0;i < array.Length; i++) { System.Console.WriteLine("" + i + "\t" + array[i]); } } private void initspA() { Q[0] = 0.0; R[0] = 0.0; for(int k = 1; k < IWINDO - 1; k++) { double P = Q[k - 1] / 2.0 + 2.0; Q[k] = -0.5 / P; R[k] = (3.0 * (A[k + 1] - 2.0 * A[k] + A[k - 1]) - R[k - 1] / 2.0) / P; } R[IWINDO - 1] = 0.0; for (int k = IWINDO - 2; k > 0; k--) { R[k] = Q[k] * R[k + 1] + R[k]; } } private void initspHC() { Q[0] = 0.0; R[0] = 0.0; for (int k = 1; k < IWINDO - 1; k++) { double P = Q[k - 1] / 2.0 + 2.0; Q[k] = -0.5 / P; R[k] = (3.0 * (HC[k + 1] - 2.0 * HC[k] + HC[k - 1]) - R[k - 1] / 2.0) / P; } R[IWINDO - 1] = 0.0; for (int k = IWINDO - 2; k > 0; k--) { R[k] = Q[k] * R[k + 1] + R[k]; } } private double splineA(double X) { int J = ifrac(X); double XX = X - J; return A[J - 1] + XX * ((A[J] - A[J - 1] - R[J - 1] / 3.0 - R[J] / 6.0) + XX * (R[J - 1] / 2.0 + XX * (R[J] - R[J - 1]) / 6.0)); } private double splineHC(double X) { int J = ifrac(X); double XX = X - J; return HC[J - 1] + XX * ((HC[J] - HC[J - 1] - R[J - 1] / 3.0 - R[J] / 6.0) + XX * (R[J - 1] / 2.0 + XX * (R[J] - R[J - 1]) / 6.0)); } public static int ifrac(double R) { return (int)Math.Floor(R); } } }補間用のジオイドモデルのデータとしてArrayHというインターフェースを実装した2種類のクラスを用意しています。
ArrayH.csusing System; using System.Collections.Generic; using System.Text; namespace GeoidInterpolateLib { public interface ArrayH { public void LoadData(string path); public void Dispose(); public float GetH(int i, int j); } }ArrayImpl.csusing System; using System.Collections.Generic; using System.Text; using System.IO.MemoryMappedFiles; namespace GeoidInterpolateLib { public class ArrayImpl : ArrayH { private float[] H = new float[GeoidInterpolateEGM96.NLAT * GeoidInterpolateEGM96.NLON]; public void Dispose() { H = null; } public float GetH(int i, int j) { return H[j + i * GeoidInterpolateEGM96.NLON]; } public void LoadData(string path) { using (var mmf = MemoryMappedFile.CreateFromFile(path)) { using (var accessor = mmf.CreateViewAccessor(0, H.Length * 4)) { for (int i = 0; i < H.Length; i++) { H[i] = accessor.ReadSingle(i * 4); } } } } } }DictImpl.csusing System; using System.Collections.Generic; using System.Text; using System.IO.MemoryMappedFiles; namespace GeoidInterpolateLib { public class DictImpl : ArrayH { private MemoryMappedFile mmf; private MemoryMappedViewAccessor accessor; private Dictionary<int, float> hDic = new Dictionary<int, float>(100); public void Dispose() { mmf.Dispose(); accessor.Dispose(); } public float GetH(int i, int j) { int index = j + i * GeoidInterpolateEGM96.NLON; float f; if (!hDic.TryGetValue(index, out f)) { f = accessor.ReadSingle(index * 4); hDic.Add(index, f); } return f; } public void LoadData(string path) { mmf = MemoryMappedFile.CreateFromFile(path); accessor = mmf.CreateViewAccessor(0, GeoidInterpolateEGM96.NLAT * GeoidInterpolateEGM96.NLON * 4); } } }内部のデータの持ち方として、ArrayとDictionaryで作り分けています。
これは、自分のいる場所のジオイド高を取得するなどの用途だと、地球全体のデータは必要ないので必要な部分だけ読み込むDictで、地球全域の座標データを変換する用途などの時は、あらかじめ全データを読み込んでおいた方が効率が良いのではということで、Arrayの方を使うとよいのではないかということで作り分けています。また、ジオイドモデルのデータは、あらかじめ以下のプログラムで変換しておいて使います。
DataConvert.csusing System; using System.Collections.Generic; using System.Text; using System.IO; using GeoidInterpolateLib; namespace DataConvert { class DataConvert { private float[] H = new float[GeoidInterpolateEGM96.NLAT * GeoidInterpolateEGM96.NLON]; private void LoadData() { StreamReader sr01 = new StreamReader(@"WW15MGH.GRD"); string headerLine = sr01.ReadLine(); string line; double[] data = new double[GeoidInterpolateEGM96.NLAT * GeoidInterpolateEGM96.NLON]; int ii = 0; while ((line = sr01.ReadLine()) != null) { string[] tokens = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); foreach (string token in tokens) { data[ii++] = double.Parse(token.Trim()); } } for (int i = 0; i < GeoidInterpolateEGM96.NLAT; i++) { for (int j = 0; j < GeoidInterpolateEGM96.NLON - GeoidInterpolateEGM96.NBDR; j++) { H[j + GeoidInterpolateEGM96.NBDR / 2 + (GeoidInterpolateEGM96.NLAT - i - 1) * GeoidInterpolateEGM96.NLON] = (float)data[j + i * (GeoidInterpolateEGM96.NLON - GeoidInterpolateEGM96.NBDR)]; } for (int k = 0; k < GeoidInterpolateEGM96.NBDR / 2; k++) { H[k + (GeoidInterpolateEGM96.NLAT - i - 1) * GeoidInterpolateEGM96.NLON] = (float)data[GeoidInterpolateEGM96.NLON - GeoidInterpolateEGM96.NBDR - GeoidInterpolateEGM96.NBDR / 2 + k + i * (GeoidInterpolateEGM96.NLON - GeoidInterpolateEGM96.NBDR)]; H[k + GeoidInterpolateEGM96.NLON - GeoidInterpolateEGM96.NBDR / 2 + (GeoidInterpolateEGM96.NLAT - i - 1) * GeoidInterpolateEGM96.NLON] = (float)data[k + i * (GeoidInterpolateEGM96.NLON - GeoidInterpolateEGM96.NBDR)]; } } } public void WriteData() { BinaryWriter bw = new BinaryWriter(new FileStream(@"ww15mgh.bin", FileMode.OpenOrCreate, FileAccess.Write)); foreach (float f in H) { bw.Write(f); } bw.Close(); } static void Main(string[] args) { var dc = new DataConvert(); dc.LoadData(); dc.WriteData(); } } }これで、iOSでは、生のセンサ値に近い楕円体高を計算できますし、Androidで標高を計算することもできるようになります。
おわりに
なぜこんなことをやっているかなのですが、ARを街中で使うときに、この辺りの計算ができると色々便利じゃないかなというところが、最初の動機になります。
さまざまな情報をより正確に扱えることは悪いことではないと思いますので、皆さんも機会があれば参考にしてみてください。
- 投稿日:2020-11-30T18:29:23+09:00
THETAの姿勢を利用し、スマホなし設定を可能にする
この記事は RICOH THETA Advent Calendar 2020 の初日記事です。
このアドベントカレンダーはどなたでも参加できます(コミュニティに入る必要はありません)
まだまだ隙間が開いています、THETAプラグインに限らずRICOH THETAにまつわる色々なことで参加して頂けると嬉しいです!はじめに
リコーの @KA-2 です。
弊社ではRICOH THETAという全周囲360度撮れるカメラを出しています。
RICOH THETA VやRICOH THETA Z1は、OSにAndroidを採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます(詳細は本記事の末尾を参照)。今回は、カメラ本体の姿勢を利用してRICOH THETA Z1の操作系を拡張します。RICOH THETA Vでもある程度は類似のことができると思いますが、OLEDがついているほうがめいっぱい操作系を拡張できるので対象機種を絞りました。RICOH THETA Vへの応用やRICOH THETA Z1用でも「自分ならこうする」という異なったバリエーション作成にトライして頂けたら幸いです。
つくったものを動作させている様子はこんな感じです。
◤Qiita記事公開◢
— THETAプラグイン開発者コミュニティ (@thetaplugin) December 1, 2020
RICOH THETAアドベントカレンダー2020初日記事公開!
カメラ本体を「傾ける」「回す」「捻る」とボタン操作を組み合わせ、スマートフォンなしで殆どの撮影設定ができるプラグインを作ったよ!https://t.co/1Q7ZKAZd4y#THETA #thetaplugin #ThetaZ1 pic.twitter.com/UspTQc9lk5今回紹介するTHETAプラグインは、アドベントカレンダー2020の期間中にストア公開を目指しています。
便利なのでつかってほしい!公開になったら改めて記事更新をします。作ったものの仕様
本プラグインでできることは以下のとおりです。
「スマホいらず」を目指したのでトコトン設定できます。
- 撮影モード(image/video)の切り替え
- 露出モード(Auto/Av/Tv/Iso/Manual)の切り替え
- 各露出プログラムにおける撮影設定
(絞り、シャッター速度、ISO感度、ホワイトバランス、静止画autoモードにおけるOption setting[NR/DR Comp/HDR/Hh HDR] )- ホワイトバランス の指定方法(プリセット/色温度)の切り替え
- 静止画撮影時におけるファイル保存形式(JPEG/RAW+)の切り替え
- セルフタイマーのOn/Off切り替え
- 連続撮影(Off/Time Shift/Interval/Interval Composite)の切り替え
その他の細かな所として、以下の事項もケアしてあります。
- このプラグインで行った設定はプラグイン終了時に保存され、再びプラグインを起動したときに復帰します。「スーパーMYセッティング」的にも利用できます。
- 本プラグインはリモートコントローラー( "Vol+"のキーコードを送出するHIDデバイス)に対応しています。
以降では、動画の解説になってしまいますが、このプラグインの振る舞いをひとつづつ説明していきます。基本的には、
- ボタンを押さないとパラメータを変更しない
(「JPEG<->RAW+切り替え」や「ホワイトバランスの指定方式選択」はちょっと例外ですが、簡単には変わらなくしていたり、表示をみると一撃で確認できる配慮をしています)- 頻繁に操作しない事項は「逆さま」にしないと変更できない
ということに配慮して操作仕様を決めています。
"ワタシ仕様"なので、お好み操作系に合わない点もあるかもしれません。どうしても自分用に変更したい方は、ソースコードも公開していますので、ご自身でカスタマイズしてくださいませ。
傾け操作による変更可能項目の選択方法
THETAを直立させた姿勢を基準とし、「45度以上傾けたあと元の姿勢に戻す」という操作で、OLED右側 二重線枠 の中の表示項目が切り替わります。凝った仕掛けにしてない(姿勢判定にヒステリシスを持たせる等していない)ので45度付近でプルプルしてしまうと切り替わりますが実用上問題にならないレベルです。
「傾ける方向」によって、どの項目を表示するか選べます。露出プログラムによって、傾ける方向と表示項目の対応を以下のように変えています。OLEDに「傾けガイド」を表示してますので迷うことはないと思います。ボタン操作
逆さま以外の姿勢では、前述の「傾け操作で選んだ項目の値」を変更できます。ボタン長押しによる連続変更に対応したかったのでModeボタンは「露出プログラムの選択」に割り当てています(※Modeボタン長押しはプラグイン終了操作に固定されています)。
逆さま姿勢(直立を基準とすると 135度以上傾けていると逆さまとみなされます)のボタン操作は、プラグイン起動前の操作方法を踏襲しています。ModeとFnの振る舞いは同じです。これで迷わない。
WLANを使わないことがミソのプラグインなので、WLANボタンには「特殊撮影の切り替え」を割り当てました。ホワイトバランス指定方法の変更
前述の傾け操作で「ホワイトバランスを表示しているとき」だけ、ホワイトバランスの指定方法(プリセット/色温度)を切り替えられるようにしました。
頻繁に変更しないと思いますので「逆さま」を絡めて変わりにくくしてあります。(とはいえ、撮影前に目視確認することはお勧めしておきます)静止画ファイル形式の変更
直立姿勢を基準として、右または左に、すばやく捻り操作をすると、ファイルフォーマットの選択をできるようにしました。「すばやく捻る」のあたりは、「200msecの間に 90度以上の回転」としてあります。
使わずに想像すると難しそうですが、やってみると簡単に変更できます。かといって簡単に変わりすぎない絶妙な仕上がりです。OLEDの表示
OLEDの表示を以下のようにしました。
キツキツです。もうこれ以上仕様を盛りたくないかんじです。
(RICOH THETA Z1の"個性"にはあっていると思います。普通のカメラっぽい表示です。)「TimeShiftとセルフタイマーは独立してOn/Offできるのに、なんで表示を重ねているのだ?」という疑問を持った方は上級者ですね。
「どちらもOn設定となったときTimeShiftが優先される」という振る舞いでしたので、このような表示としています。ちょっと凝っているのが以下の点です。
- タイムシフト撮影中とインターバル撮影中は、ダイアログに「in progress.」を表示します。
- インターバル合成中は、ダイアログに"経過時間"を表示します。
- 動画撮影中は、ダイアログに"記録時間"を表示します。
- 露出プログラムがマニュアルのとき、設定値に連動したOLEDの輝度調節をします。
ダイアログ表示は以下のとおり。
最後のポイントは完全に私的仕様です。私は「暗いところでマニュアル撮影するときに、表示輝度が高いことがイヤ」なので、凝った作りにしました。
(絞り、シャッター速度、ISO感度のどれを操作しても、0.3段あたり1ステップ、OLEDの輝度を変化させています。カメラに多く光を取り込む設定であるほどOLEDを暗く、逆の場合はOLEDを明るく制御しています)制約事項
- Option setting(_filter)の表示順番は、 RICOH THETA APIの応答により決定しています。
- インターバル撮影とインターバル合成の終了に時間がかかります。終了操作後、1回程度は撮影を続けますがしばらくお待ちください。
- インターバル撮影の設定は「撮影間隔=最短」「撮影期間=∞(=0)」固定です。
- インターバル合成の設定は「途中経過保存=なし」「撮影時間=24時間」固定です
- セルフタイマーの時間は、基本アプリから設定してください。
傾け操作の実現方法
過去の記事「NDKでEquirectangularの回転処理をして物体を全方位トラッキングする」でも説明していますが、今回必要なところだけ抜き出して再掲載しておきます。
Android標準ライブラリの姿勢情報を得るメソッドSensorManager.getOrientationを利用しています。
一般的なAndroidスマートフォンにおけるローカル座標系と、THETAにおけるローカル座標系は、それぞれ以下のように定義されています。Googleのドキュメントを読むとき、これらの差異に注意してください。
そして、ψ=方位角(Azimath/Yaw)、θ=勾配(Pitch)、φ=回転(Roll)の値域には、以下のような差異があります。
ψの値域 θの値域 Φの値域 一般的なスマートフォン -π~π -π~π -π/2~π/2 THETA -π~π -π/2~π/2 -π~π θとΦはいずれか一方の値域が-π/2~π/2、もう一方が-π~πになっていればよいです。
THETAは、Equirectangularという形式(地球儀と世界地図の関係における世界地図の形式)で映像が得られ、画像の縦軸がPitchを現しています。この地域が-π/2~π/2であるため、対応がとれるようにしてあります。直感的にもわかり易いです。刻々と姿勢が3軸の角度で得られますので、それにあわせて、所望の動作をするコードを書くだけです。とても簡単です。
ソースコード
こちらのプロジェクト一式を参照してください。
ポイントを絞った詳細な説明を以下に記載します。ファイル構成
RICOH THETA APIを利用して各種撮影設定を行うことから、すでに殆どの撮影設定とOLED表示を網羅していたこちらの記事のソースコードをベースに作成しました。
新規作成、または、変更を加えたファイルは以下のとおりです。
theta-plugin-switchbot-link\app\src\main ├assets // OLED表示の部品が入っています。いくらか新規作成アイコンがありますが見栄えのことですので説明は割愛します。 └java\com\theta360 ├pluginapplication │ ├model // THETA Plug-in SDKのままです。 │ ├network // 過去事例と同じようにHttpConnector.javaにメソッド追加したのみです。 │ ├oled // Oled.java(OLED描画ライブラリ的なもの)のみです。 │ └task // ChangeFilterTask.javaとChangeFormatTask.javaが新規です。 │ // GetCameraStatusTask.java, ShutterButtonTask.java, ChangeShutterSpeedTask.java, ChangeWbTask.java を修正しました。 └tiltui // Attitude.javaは、過去に作成したままです。便利クラスになってます。 // MainActivity.java, DisplayInfo.java に本プラグイン独自事項が盛り込まれています。傾き判定の方法
以下3つの基礎的事項は、リンク先を参照してください。
プログラムの基本構造
MainActivity.javaの中に周期動作をするスレッドを作成し、以下のような処理を行っています。WebAPIで撮影タスクの状態を周期的に取得するので、内部の通信負荷が高くなりすぎないよう、200msecのお休みをしながら動作させるようにしています。
MainActivity.javapublic void drawOledThread() { new Thread(new Runnable() { @Override public void run() { //ループ手前でやることがあるならば・・・ //描画ループ while (mFinished == false) { //ステータスチェックと描画 try { //姿勢情報取得&姿勢関連情報更新 displayInfo.setTiltStatus(attitude.getDegAzimath(), attitude.getDegPitch(), attitude.getDegRoll()); //内部状態獲得 new GetCameraStatusTask(mGetCameraStatusCallback).execute(); // OLED表示 displayInfo.displayTiltUI(); // 逆さま戻しあり && WB変更可能 -> Preset <-> CT 切り替え ~省略~ //捻り操作検出時の処理 ~省略~ //ボタン操作の実行 ~省略~ //もろもろが高頻度になりすぎないようスリープする Thread.sleep(200); } catch (InterruptedException e) { // Deal with error. e.printStackTrace(); } finally { // } } } }).start(); }姿勢の判定
上記周期動作スレッドの先頭で呼び出している、DisplayinfoクラスのsetTiltStatusメソッドで行っています。
処理内容は以下のとおり。DisplayInfo.javavoid setTiltStatus(double inYaw, double inPitch, double inRoll) { beforDegAzimath = degAzimath; degAzimath = inYaw; degPitch = inPitch; degRoll = inRoll; beforTilt = curTilt; if ( (-45 <= degPitch) && (degPitch<45) ) { if ( (-45<=degRoll) && (degRoll<=45) ) { curTilt = TILT_BASE; } else if ( (45<=degRoll) && (degRoll<=135) ) { curTilt = TILT_RIGHT; } else if ( (-135<=degRoll) && (degRoll<=-45) ) { curTilt = TILT_LEFT; } else if ( ((135<=degRoll) && (degRoll<=180)) || ( (-180<=degRoll) && (degRoll<=-135) ) ) { curTilt = TILT_UPSIDE_DOWN; occurUpsideDown=true; } else { curTilt = TILT_ERROR; } } else if ( (45 <= degPitch) && (degPitch<=90) ) { curTilt = TILT_FRONT; } else if ( (-90 <= degPitch) && (degPitch<=-45) ) { curTilt = TILT_BACK; } else { curTilt = TILT_ERROR; } //捻りイベントチェック if ( curTilt == TILT_BASE ) { double diffAzimath = beforDegAzimath-degAzimath; if ( diffAzimath >= TWIST_THRESH ) { lastEvent = EVENT_TWIST_L; } else if ( diffAzimath <= -1.0*TWIST_THRESH) { lastEvent = EVENT_TWIST_R; } else { //捻りイベントはなし } } //傾きイベントチェック boolean occurChangeEvent = false; if ( (curTilt==TILT_BASE)&&(beforTilt!=TILT_BASE) ) { occurChangeEvent = true; if (occurUpsideDown) { occurUpsideDown=false; lastEvent =EVENT_UPSIDE_DOWN; } else { switch (beforTilt) { case TILT_FRONT : lastEvent =EVENT_FRONT; break; case TILT_BACK : lastEvent =EVENT_BACK; break; case TILT_RIGHT : lastEvent =EVENT_RIGHT; break; case TILT_LEFT : lastEvent =EVENT_LEFT; break; default: //無処理:起こらないはずが念のため break; } } // 変更可能パラメータの遷移 ~省略~ } }最初に、「現在の姿勢」を判定したあと、周期動作の中の前回姿勢からの変化によって「イベント」を設定しています。この処理のあとに続く処理では、イベントに対応する振る舞いをしているだけです。
書くのが面倒なだけで、難しくはありません。撮影設定を行うタスクの共通構造
本プラグインでは、多種類の撮影設定ができ、設定項目ごとにタスクを11種類用意しています。各タスクの処理が終わった後には共通のCallback処理を行わせたかったので、AndroidのConsumerという仕組みを利用しています。コールバックだらけにならずスッキリ記述できています。
まとめ
「RICOH THETAはスティック形状」ということも相まって、とてもよい操作拡張が行えたと思っています。
RICOH THETA Z1ユーザーは、「スマートフォンなしで撮影設定をしたい」という方がわりと多くいるのではないでしょうか?ストア公開を頑張りますので、ぜひとも使ってください。類似の操作系をRICOH THETA Vで行う場合、CaptureModeと露出プログラムは決めうちしたapkとしたほうがよさそうですね。表示はLED3の色を駆使し、設定値お知らせは音声ファイル再生で読み上げることができますが、ボタンが少ない点がしんどそーです。
とはいえ、是非ともチャレンジしてみてください!<余談>
このプラグインの超裏技的な使い方に気づいてしまいました。
このプラグインは設定値を記憶できるプラグインです。applicationIdとapp_name(プラグイン名称)を変えただけのapkを複数作ってインストールすると、その数だけマイセッティングが増やせることになります。
そして、「Plug-in Launcher for Z1」を併用すると、名前違いのプラグインが沢山あっても直ぐに呼び出せるという、そんな使い方までできてしまいます。
(もしかして、名前違いのapkを5つくらいストア置いてもニーズあったりするのかしら・・・)
Twitterアカウントやslackなどを通して、このプラグインへのご意見ご要望なども頂けたら幸いです。RICOH THETAプラグインパートナープログラムについて
THETAプラグインをご存じない方はこちらをご覧ください。
パートナープログラムへの登録方法はこちらにもまとめてあります。
QiitaのRICOH THETAプラグイン開発者コミュニティ TOPページ「About」に便利な記事リンク集もあります。
興味を持たれた方はTwitterのフォローとTHETAプラグイン開発者コミュニティ(Slack)への参加もよろしくおねがいします。
- 投稿日:2020-11-30T18:15:54+09:00
Jetpack Composeがalpha版になったので改めてQiitaビューアを作ってみた
この記事はNTTテクノクロス Advent Calendar 2020の4日目です。
こんにちは、NTTテクノクロスの戸部@etctaroと申します。
普段は社内でモバイルアプリ開発関連の技術支援や社内向けのノウハウ記事執筆、社内研修講師活動、ハンズオン会の開催などを行っています。はじめに
- Jetpack Composeについては昨年の記事でも取り上げました。
- 一言で述べるなら、「Androidアプリ開発における新しいUIの実装方法」となります。
- 詳しく知りたい方は公式サイトを読んでいただいたり、CodeLabで手を動かしたりしてみていただいても良いかと思います。
- 昨年はプレビュー版が出たてホヤホヤでしたが、今年はアルファ版になり、少しずつ実用化が近づいてきた印象があります。
- そんなこんなで昨年の記事も振り返りつつ、現在の仕様に合わせて再びQiitaビューアを作ることにいたします。
注意点
- Android Studio 4.2はCanary16版、Jetpack Composeは1.0.0-alpha07版を使います。
- いずれもバージョンアップなどで変更が入る可能性が十分に想定されますのでご留意ください。
- などと書いていたら、Android Studioの新しいバージョンArctic Foxがリリースされ無事フラグを回収しました。
- マイグレーションについてはこちらを参照してください。
- ※なお、以下は動作確認のための最低限の実装となりますので、本格実装するのであればリファクタリングの呼吸壱の型全集中でお願いします。
目標
- まずは昨年と同等のQiitaビューアを作る
- あとは時間の許す限り、Jetpack Composeのユニットテストの実行などいろいろ遊ぶ。
ところで昨年のコードは?
- 昨年の記事の中で作成したコードについては、アップグレードが必要、というメッセージとともに各種ライブラリ類を更新することになりました。
- State周りの仕様が変わっているのでビルドに失敗するはず・・・と思っていましたが無事エラーとなりました。
- (と、書きつつも、一から作り直す想定だったのでどうしようということもありませんが。)
前提条件
以下の環境を前提とします。
- Android Studio 4.2 (Canary 16)
- Target Sdk Version 29
- 通信、シリアライズについては、Ktorおよびkotlinx.serializationを使います。(そしてこの選択が鬼門に。)
昨年からの更新内容
- Stateについては最新の仕様に対応する。
- リスト要素のクリックはModifier.clickableが使える。昨年はClickableで囲んでいた。
- リストのスクロール(縦方向)はScrollableColumnが使える。昨年はFlexColumnとVerticalScrollerを必要としていた。
- FABの配置はScaffoldに任せることにした。
- Contextの取得は無理矢理やらずにContextAmbientを利用 (参考)
- UIテストに全集中
大ハマりポイント
今年も相変わらず無事ハマりました。と言っても、1つ大きくハマったくらいで、あとは公式のガイドなどをみつつ進めていけたようには思います。
- kotlinx.serialization + Jetpack Composeの組み合わせを同じモジュール内で実装した場合、ビルドが終わらない
- Android Studioの新バージョンがリリースされた
kotlinx.serialization + Jetpack Composeの組み合わせを同じモジュール内で実装した場合、ビルドが終わらない
- Ktorで通信してその結果をシリアライズ、画面に反映する、という割とやりがちなことをやろうとしたところ、そもそもビルドのタスクが終わらないという状況に。
- 被疑箇所を探るためにdependeniesなどを一つずつコメントアウトなどしながら確認。
- 結論として、kotlinx.serializationのアノテーション(
@Serializable
など)を使ったクラスをJetpack Composeを使ったプロジェクトに含めていると、ビルドできないことがわかりました。- Jetpack Composeのコードで実際に使っていなくても、Entityのクラスが同じモジュールにあるだけでもNGなようです。(少なくとも私の環境では。)
おや、と思いつつ、issueを探していたらまさにありました
- そのうち修正するよ、とは書いていただいているので、本格的にはそれを待つことになるかもしれません。
暫定的な対応としては、同じissueの中で以下の通り書かれている通り、別のモジュールで実装するのが良さそうです。(いいね1000個差し上げたい気分)
This issue only exists when using compose+serialization if used within the same module. Temporary workaround is to just separate the components into different modules (ui/app+network/serialization).ということで、シリアライズ+通信用の処理については別のモジュールに持っていくことにしました。
- この記事が大変わかりやすかったので、そのまま参考に。
Step1: 通信、シリアライズ周りの実装
繰り返し述べているように、今回は
kotlinx.serialization
、Ktor
を使います。
以下、バージョンについては一例です。状況に応じて最新化などが必要になります。また、ハマりポイントでも書いた通り、この処理はUIとは別モジュールとしました。
moduleのbuild.gradleの変更
Serialization
のプラグインを追加します。plugins { ... id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.10' // 追記 }また、以下の依存関係を追加します。(モジュールの
build.gradle
)dependencies { ... // Ktor implementation "io.ktor:ktor-client-core:1.4.1" implementation "io.ktor:ktor-client-serialization:1.4.1" implementation "io.ktor:ktor-client-android:1.4.1" // Serialization implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1" }Entityの作成
kotlinx.serialization
を使ったdataクラスにしました。@Serializable data class QiitaArticle ( @SerialName("id") val id: String, @SerialName("title") val title: String, @SerialName("url") val url: String )通信部分の実装
Qiitaの情報を取得するためにAPIを叩きます。
QiitaのAPIからはJSONの形で結果を受け取るので、必要な要素だけシリアライズします。
また、HttpClientにはengineの設定が必要になります。エラーメッセージにも表示されますが、ここを参考にしました。class QiitaApi { private val httpClient = HttpClient(Android) { engine { connectTimeout = 100_000 socketTimeout = 100_000 } install(JsonFeature) { val json = kotlinx.serialization.json.Json {ignoreUnknownKeys = true} serializer = KotlinxSerializer(json) } } companion object { private const val QIITA_URI = "https://qiita.com/api/v2/items?page=1&per_page=20" } suspend fun getAllArticles(): List<QiitaArticle> { return httpClient.get(QIITA_URI) } }UI側でこのモジュールをdependencyに追加する
dependencies { implementation project(":mylibrary") (以下略) }Step2: アプリのUI部分を実装する
さて、ここからはJetpack Composeの世界になります。
一つの記事を表すComposable
- タイトルとURLを縦並びで表示し、これを1つの記事として表示します。
- タップ時の処理をModifier.clickableで実装する点が昨年との違いです。これは楽。
@Composable fun QiitaItem(title: String, url: String) { Row(Modifier.clickable(onClick = {})) { Column() { Text(text = title) Text(text = url) } } }複数の記事の一覧を表すComposable
- 前項のQiitaItemをさらに縦並びで表示します。
- スクロール可能な一覧とする場合は、
ScrollableColumn
を使うことで実現ができます。これもとても楽になりました。@Composable fun QiitaItemList(items: List<QiitaArticle>) { ScrollableColumn { for (item in items) { QiitaItem(title = "${item.title}", url = "${item.url}") Divider(color = Color.Black) } } }一画面分のComposableの実装
- ここまでで実装したComposableと、FABを組み合わせます。
Scaffold
を使うことでFABも手軽に配置することができます。- FABのクリック時の処理などについては後述します。
@Composable fun HomeScreen() { MaterialTheme { Scaffold( bodyContent = { QiitaItemList(items = listOf()) }, floatingActionButton = { FloatingActionButton( onClick = {} ) { Text("reload") } } ) } }Step3: State(状態)の管理
- 昨年は
@Model
のアノテーションを付与することで監視可能な状態を表していましたが、現行の仕様では使用できません。
- ※昨年のハマりポイントではModelListなるクラスを使っていましたが、こちらは現在はAPI上存在しません。
- ※Stateについての公式ドキュメントはこちら
- 監視可能なリストについては、mutableStateListOfを使うことができます。本記事ではこれを使います。
- なお、State管理については他にも方法がありますが、ここでは特に考慮しません。
@Composable fun HomeScreen() { val qiitaArticles = remember {mutableStateListOf<QiitaArticle>()} MaterialTheme { Scaffold( bodyContent = { QiitaItemList(items = qiitaArticles) }, floatingActionButton = { FloatingActionButton( onClick = { } ) { Text("reload") } } ) } }Step4: FABの処理
- FABをタップした際の処理を実装します。
- 通信を実行する
- APIの結果を取得する
- 一つ前の項で用意したqiitaArticlesに対して、取得したQiitaArticleの一覧を反映します。
- qiitaArticlesに変更が入ると、HomeScreenが書き換えられるという状態になります。
FloatingActionButton( onClick = { MainScope().launch { try { val articles = QiitaApi().getAllArticles() qiitaArticles.clear() qiitaArticles.addAll(articles) } catch (e: Exception) { Log.e(e) } } } ) { Text("reload") }Step5: 記事クリック時の処理
- 記事をクリックした際に、そのURLをブラウザで表示しようと思います。
- 昨年は結構無茶な実装をしましたが、Contextの取得は無理矢理やらずに
ContextAmbient
を利用すれば比較的容易にできます。Intent
を投げるだけなら、これを使えば良いです。- ということで、
QiitaItem
のRow
のonClick
について実装を行います。@Composable fun QiitaItem(title: String, url: String) { val context: Context = ContextAmbient.current Row(Modifier.clickable(onClick = { val uri = Uri.parse(url) val intent = Intent(ACTION_VIEW, uri) startActivity(context, intent, null) })) { Column() { Text(text = title) Text(text = url) } } }Step6: スタイルの変更
- 公式のチュートリアルなどを参考に、QiitaItemの一行の表示を変えてみました。(サンプル通り。)
- こちらも参考になります:Compose でのテーマ設定
Column() { Text(text = title, style = typography.h6) Text(text = url, style = typography.body2) }ここまでの完成形
もう少し変更・試してみる
FABをカスタマイズ
- FloatingActionButtonはアイコンを一つ表示するのに使われるようで、デフォルトだと円になりました。(この点は昨年と違う。)
- 文字列のように少し横長にした方が良い場合はExtendedFloatingActionButtonを使います。
- 本来はアイコンだけで使い方が伝わった方が良いかもしれませんが。
floatingActionButton = { ExtendedFloatingActionButton( onClick = { // 略 }, text = {Text("reload")} ) }色味はともかく、去年と同じように文字に合わせて横長になりました。
- テキストとアイコンを並べることもできます。
floatingActionButton = { ExtendedFloatingActionButton( onClick = { // 略 }, text = {Text("reload")}, icon = {Icon(asset = Icons.Filled.Refresh)} ) }UIテスト
昨年は諦めたUIテストですが、Jetpack Composeでも動かせるようになりつつあるので、試してみました。
- dependencyの追加
dependencies { (略) // forTest androidTestImplementation("androidx.ui:ui-test:$compose_version") }
- プロダクトコードへのラベル追加
Jetpack ComposeのUIテストを実施する場合、Composableのテキストを使って探すこともできますが、そもそもテキストを持たない場合(例えばアイコンやレイアウト用のComposableなど)は探せません。
このため、プロダクトコードに対してAccessibilityのテキストを付与することで、テストコード側でもこのComposableを探すことが可能となります。
ScrollableColumn( modifier = Modifier.semantics {accessibilityLabel = "Item List"}) { (略) } ExtendedFloatingActionButton( modifier = Modifier.semantics {accessibilityLabel = "Refresh Button"} )
- テストコードの作成
class MainActivityTest { @get:Rule val composeTestRule = createAndroidComposeRule<MainActivity>() @Test fun test_RefreshButton_tap_once() { composeTestRule.setContent { SampleCompose100AlphaTheme { HomeScreen() } } // Nodeのツリーを出力 composeTestRule.onRoot().printToLog("ForComposeUITest") // FABをAccessibilityで探す→タップ composeTestRule.onNodeWithLabel("Refresh Button").performClick() // とりあえず少し待つ Thread.sleep(5000) // Nodeのツリーを再び出力 composeTestRule.onRoot().printToLog("ForComposeUITest") // Columnを探す→子が20個あるはず composeTestRule.onNodeWithLabel("Item List").onChildren().assertCountEquals(20) } }
- その他ハマったポイント
こんなエラーメッセージが出てテスト実行時に失敗しました。
2 files found with path 'META-INF/AL2.0' from inputsメタ情報が重複している、ということですが、appの
build.gradle
に以下を追加して対応。android { packagingOptions { exclude 'META-INF/AL2.0' exclude 'META-INF/LGPL2.1' } }
UIテストについての雑感
公式のドキュメントにも書かれていた通り、Espressoに近い、というのはまさしくその通りだと感じました。
ただ、若干情報が不足している感はあり、ラベル付けの話などは自前で試行錯誤してみた結果です。
(一応、APIはあったので、多分できるんだろうと思ってはいましたが。)補足: Android Studioの更新対応(to Arctic Fox)
この記事を書いている時に丁度2020.3.1 Arctic Fox Canary1のリリースがありました。ここではマイグレーションについて補足します。
- ポイント
- Arctic Foxでプロジェクトを動かす場合はGradle Pluginの更新が必要
- 新しいGradle Pluginで動かすためにはJDK11が必要
- マイグレーションはAndroid Studioの指示に従うと楽
Arctic Foxでプロジェクトを動かす場合はGradle Pluginの更新が必要
- Arctic Foxでは古いバージョン(4.2以前)のGradle Pluginは動作しません。ビルドしようとすると以下の通りエラーメッセージが表示されます。
The project is using an incompatible version (4.2.0-alpha16) of the Android Gradle plugin.
- このため、バージョンをArctic Foxが対応する7系以降に変更する必要があります。
- 新規にプロジェクトを作成した際には、rootのbuild.gradleは以下のように設定されています。このように設定してください。
dependencies { classpath 'com.android.tools.build:gradle:7.0.0-alpha01' ...
- また、対応するGradleの最小バージョンが6.7.1となりますので、こちらも更新が必要です。(
gradle/wrapper/gradle-wrapper.properties
)distributionUrl=https://services.gradle.org/distributions/gradle-6.7.1-bin.zipなお、以前のバージョンのAndroid Studioで動かす場合は、プラグインのバージョンの設定を対応する値に戻す必要があります。
新しいGradle Pluginで動かすためにはJDK11が必要
- Gradle Pluginの7.0.0-alpha01(おそらくそれ以降も)を動かすためにはJDK11が必要です。8を利用していた場合は以下のエラーメッセージが表示されます。
Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8. You can try some of the following options: - changing the IDE settings. - changing the JAVA_HOME environment variable. - changing `org.gradle.java.home` in `gradle.properties`.マイグレーションはAndroid Studioの指示に従うと楽
- プラグインの更新が必要なプロジェクトを開いた場合、またはビルド時のエラーメッセージで
Upgrade to the latest version
というリンクが表示されるので、それをクリックするとUpgradeを促されます。
- 折角勧められているのでこれに従ってみますと以下の変更が入ります。
- Gradleのバージョンが6.7.1に更新。
- Gradle Pluginのバージョンが7.0.0-alpha01に更新。
この記事で作成したサンプルコード
- 作成したサンプルコードはGitHubに掲載しています。
おわりに
- ということで、今年もつらつらとJetpack Composeについて書いてきました。
- 昨年やり残したUIテストもついに実行できてAdvent Calendarの記事としては満足なところです。
- これからJetpack Composeがより広がっていくことになっていくと思いますので、そんな時に少しでも役に立てれば良いと思います。
- ということで、皆様、良いJetpack Composeライフを。
令和TXコソコソ噂話
5日目は武井@thetakeiさんが開発とセキュリティの関係について語ってくださるようですよ。
明日もぜひご覧ください!
- 投稿日:2020-11-30T11:57:41+09:00
[Delphi] Android の Back ボタンでアプリを終わらせない方法
Delphi 製 Android アプリ
Delphi で Android のアプリを作ると「バックキー」でアプリ終わっちゃうんだよね!
Android 使いなら解ってくれると思うけど、ホームキーはあんまり押さないで、バックキーを連打するじゃないですか!え、しませんか!?
で、バックキーを連打で終わらせると Delphi のアプリ、次に起動した時またスプラッシュから~!?となり割とイラッとするので、僕が作るアプリは基本終わらないようにしています。終わらせないコード
- バックキーは TForm.OnKeyUp イベントで取得可能
- Activity#moveTaskToBack を呼ぶとアプリを裏に回す(ホームキーを押した時と同じ動作)
この2つを合せて↓下記の様にします。
uses Androidapi.Helpers; procedure TForm1.FormKeyUp( Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState); begin {$IFDEF ANDROID} if Key = vkHardwareBack then // Android バックキーの仮想キーコード begin // Key, KeyChar を 0 にしないとデフォルトの動作が呼ばれ Activity が閉じてしまう Key := 0; KeyChar := #0; // アプリを裏に回す TAndroidHelper.Activity.MoveTaskToBack(True); end; {$ENDIF} end;実質たった8行だけで Activity が閉じなくなるので常に書いておくことをオススメします。
これで、アプリがメモリにある間は起動しても前回の続きから表示されます。戻ってきたとき再描画させる
ただ、これだけだとたまに戻ってきたときにアプリの内容が表示されない(真っ白の画面が表示されるような状態)場合があります。
これを防ぐためには IFMXApplicationEventService を使います。
TForm.OnCreate などで設定しておくと良さそうです。uses FMX.Platform; procedure TForm1.FormCreate(Sender: TObject); begin var Service: IFMXApplicationEventService; if TPlatformServices.Current.SupportsPlatformService( IFMXApplicationEventService, Service ) then // 下の AppEvent を登録 Service.SetApplicationEventHandler(AppEvent); end; function TForm1.AppEvent( iAppEvent: TApplicationEvent; iContext: TObject): Boolean; begin Result := False; case iAppEvent of // これからアクティブになるよというイベント TApplicationEvent.BecameActive: begin // Form の Invalidate メソッドを呼んで再描画をうながす Invalidate; end; end;最後に
Activity#moveTaskToBack() と Activity#finish() のどちらをバックキーのデフォルトの動作にするかプロパティで選べるようになってたら良いのになあ(Android 単体の話だから無理かな)
- 投稿日:2020-11-30T11:55:41+09:00
【Android / Kotlin】RecyclerView セルのクリックイベント(画面遷移+データ受け渡し)を実装
はじめに
リサイクラービューでセルをクリックしたときの処理を実装した際に学んだことを記事に残します。
この記事は以前書いた記事(↓↓)に続く内容となっております。
【Android / Kotlin】RecyclerView で一覧画面を実装サンプルアプリの概要
以前の記事で作成した書籍一覧画面のセル(行)をクリックすると書籍情報を表示する画面に遷移させるというもの。
※ 一覧に全て同じサンプルデータを表示していることと、同じ要素を異なるフラグメントで少し表示を変えているだけです。あくまで学習用のサンプルアプリ作成しただけなので各所至らない部分がありますがお許しください。実装
注意点
前回の記事で導入したライブラリを導入していないと後述する
setFragmentResult()
やsetFragmentResultListener()
が利用できないので注意。build.gradledependencies { // これを必ず記述しておく implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha04' }リサイクラービューアダプターにクリックリスナインターフェースを定義する
- リスナ変数を定義
- インターフェースを作成
- リスナーをセットする関数を定義
- onBindViewHolderの中にセルのクリックリスナをセット
BookListRecyclerViewAdapter.kt// コンストラクタにBookクラスを持つMutableListをセット class BookListRecyclerViewAdapter ( private val bookListData: MutableList<Book>) : RecyclerView.Adapter<BookListRecyclerViewAdapter.BookListRecyclerViewHolder>() { // 1. リスナを格納する変数を定義(lateinitで初期化を遅らせている) private lateinit var listener: OnBookCellClickListener // 2. インターフェースを作成 interface OnBookCellClickListener { fun onItemClick(book: Book) } // 3. リスナーをセット fun setOnBookCellClickListener(listener: OnBookCellClickListener) { // 定義した変数listenerに実行したい処理を引数で渡す(BookListFragmentで渡している) this.listener = listener }BookListRecyclerViewAdapter.kt// BookListRecyclerViewHolder内の各画面部品に表示したいデータを割り当てるメソッド override fun onBindViewHolder(holder: BookListRecyclerViewHolder, position: Int) { // ・・・省略 // 4. セルのクリックイベントにリスナをセット holder.itemView.setOnClickListener { // セルがクリックされた時にインターフェースの処理が実行される listener.onItemClick(book) } }ファイル全体の記述
BookListRecyclerViewAdapter.kt// コンストラクタにBookクラスを持つMutableListをセット class BookListRecyclerViewAdapter ( private val bookListData: MutableList<Book>) : RecyclerView.Adapter<BookListRecyclerViewAdapter.BookListRecyclerViewHolder>() { // リスナを格納する変数を定義(lateinitで初期化を遅らせている) private lateinit var listener: OnBookCellClickListener // インターフェースを作成 interface OnBookCellClickListener { fun onItemClick(book: Book) } // リスナーをセット fun setOnBookCellClickListener(listener: OnBookCellClickListener) { // 定義した変数listenerに実行したい処理を引数で渡す(BookListFragmentで渡している) this.listener = listener } // 画面部品要素を構成するクラスを定義 // innerを付けないことでstaticなclassとして定義できる(非staticな内部クラスは非推奨) class BookListRecyclerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { // ここではcell_book_list.xmlより各レイアウト要素を取得して変数に格納している var bookName: TextView = itemView.findViewById(R.id.tv_book_name) var bookPrice: TextView = itemView.findViewById(R.id.tv_book_price) var bookPurchaseDate: TextView = itemView.findViewById(R.id.tv_book_purchase_date) } // 画面部品を保持する自作クラスであるBookListRecyclerViewHolderのオブジェクトを生成するメソッド override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : BookListRecyclerViewHolder { val inflater = LayoutInflater.from(parent.context) val view = inflater.inflate(R.layout.cell_book_list, parent, false) return BookListRecyclerViewHolder(view) } // BookListRecyclerViewHolder内の各画面部品に表示したいデータを割り当てるメソッド override fun onBindViewHolder(holder: BookListRecyclerViewHolder, position: Int) { // positionは表示するリストbookListDataのインデックス番号のようなもの val book = bookListData[position] // BookListRecyclerViewHolderより取得したレイアウト要素に書籍情報を格納 holder.bookName.text = book.name holder.bookPrice.text = book.price.toString() holder.bookPurchaseDate.text = book.date // セルのクリックイベントにリスナをセット holder.itemView.setOnClickListener { // セルがクリックされた時にインターフェースの処理が実行される listener.onItemClick(book) } } // データ件数を返すメソッド override fun getItemCount() : Int = bookListData.size }一覧画面にクリック処理を実装
onCreateView
の中にインターフェースを実装し、セルがクリックされたときの処理を定義するBookListFragment.kt// 一部記述を抜粋 override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { // ダミーデータをセットしたアダプターを作成 val adapter = BookListRecyclerViewAdapter(createDummyBookList()) // 書籍情報セルのクリック処理 adapter.setOnBookCellClickListener( object : BookListRecyclerViewAdapter.OnBookCellClickListener { override fun onItemClick(book: Book) { // 書籍データを渡す処理 setFragmentResult("bookData", bundleOf( "bookName" to book.name, "bookPrice" to book.price, "bookPurchaseDate" to book.date )) // 画面遷移処理 parentFragmentManager .beginTransaction() .replace(R.id.fl_activity_main, BookFragment()) .addToBackStack(null) .commit() } } ) return view }クリックイベントではここでしか利用をしないため object式 でインターフェースを定義。
BookListFragment.ktadapter.setOnBookCellClickListener( // object : インターフェース { 処理 } object : BookListRecyclerViewAdapter.OnBookCellClickListener { override fun onItemClick(book: Book) { // セルがクリックされたときの処理 } } )ファイル全体の記述
BookListFragment.ktclass BookListFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_book_list, container, false) // タイトルをセット activity?.title = "書籍情報一覧" // レイアウト要素RecyclerViewを取得 val bookListRecyclerView = view.findViewById<RecyclerView>(R.id.recycler_view) // LayoutManagerを取得 val linearLayoutManager = LinearLayoutManager(view.context) // ダミーデータをセットしたアダプターを作成 val adapter = BookListRecyclerViewAdapter(createDummyBookList()) // linearLayoutManager と adapter をRecyclerViewにセット bookListRecyclerView.layoutManager = linearLayoutManager bookListRecyclerView.adapter = adapter // 一覧画面の各セルの区切り線を作成 bookListRecyclerView.addItemDecoration(DividerItemDecoration(view.context, linearLayoutManager.orientation)) // 書籍情報セルのクリック処理 adapter.setOnBookCellClickListener( // インターフェースの再利用は想定しておらず、その場限りでしか使わないためobject式として宣言 object : BookListRecyclerViewAdapter.OnBookCellClickListener { override fun onItemClick(book: Book) { // 書籍データを渡す処理 setFragmentResult("bookData", bundleOf( "bookName" to book.name, "bookPrice" to book.price, "bookPurchaseDate" to book.date )) // 画面遷移処理 parentFragmentManager .beginTransaction() .replace(R.id.fl_activity_main, BookFragment()) .addToBackStack(null) .commit() } } ) return view } // サンプルデータ作成メソッド private fun createDummyBookList(): MutableList<Book> { var bookList: MutableList<Book> = ArrayList() var book = Book("Kotlinスタートブック", 2800, "2020/11/24") // 20件のダミーデータを登録 var i = 0 while (i < 20) { i++ bookList.add(book) } return bookList } }遷移後画面を用意し、データを受け取り表示させる
新たに書籍情報を表示させるFragmentを作成し、渡されたデータを画面に表示させる
BookFragment.ktclass BookFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_book, container, false) activity?.title = "書籍情報" // 一覧画面から渡されたデータをviewに表示する setFragmentResultListener("bookData") { _, bundle -> tv_book_name.text = bundle.getString("bookName") tv_book_price.text = bundle.getInt("bookPrice").toString() tv_book_purchase_date.text = bundle.getString("bookPurchaseDate") } return view } }BookFragmentのレイアウトファイルはこんな感じ
fragment_book.xml<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/layout_book" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv_book_name" android:textSize="32dp" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.3"/> <TextView android:id="@+id/tv_book_price" android:textSize="32dp" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.5" /> <TextView android:textSize="32dp" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="@id/tv_book_price" app:layout_constraintStart_toEndOf="@id/tv_book_price" android:text="円"/> <TextView android:id="@+id/tv_book_purchase_date" android:textSize="32dp" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.7" /> </androidx.constraintlayout.widget.ConstraintLayout>最後に
インターフェースの実装や Kotlin の構文など理解し切れていない部分が多々ありますが、Androidを Kotlin で開発するのは書いていて楽しいです。
今後も学習アウトプットなど積極的に発信していきます。
誤りご指摘などあれば気軽にコメントください。
- 投稿日:2020-11-30T11:43:15+09:00
Identifying Performance bottlenecks in TensorFlow Lite models using Android Trace Api
In this article, we will introduce Android Tracing tools to capture internal events from the TensorFlow Lite interpreter which can help us identify potential issues in performance of the models.
This article assumes you know how to use Android studio to build and run apps on mobile devices.
Discussion Topics
- What is System Tracing in Android?
- Different tools to capture Trace
- How to record TensorFlow Lite events
- How to use the System Trace data
What is System Tracing in Android?
The method of recording all activity happening in a device over a short period of time is called System Tracing. Developers can use System Trace to troubleshoot the run time performance of app. It combines the data from Kernel, Android Framework, CPU scheduler, disk activity and app threads.
Current Android tracing tools support for combining the TensorFlow Lite Internal events along side Java/Kotlin code events. Some example events that can be captured by this tool are tensor allocation, operator invocation, graph modification by delegate.
Tools to capture Trace
Android platform provides different options to capture traces.
Android Studio CPU Profiler
This is one of the popular tool to investigate system performance. It is integrated directly into the Android Studio with rich interactive UI which provides real time visual information about the CPU usage and thread activity.
System Tracing App
This is a system level app that is present in devices running Android 9 (API level 28) or later. This records all events happening in the device using the systrace command-line utility without needing to connect over ADB. The trace file is saved over device and can be shared across teams.
Systrace command-line tool
This is a legacy tool to record a system trace over a short period of time and then compress it into text file.
Perfetto command-line tool
Perfetto is open-source tracing project which provides platform-wide tracing tool for Android, Linux, and Chrome. It is a more sophisticated tool and was introduced in Android 10.
CPU profiler and Perfetto provide rich user-friendly UI to visualize the trace data, which can help in identifying the performance issues and the cause very easily. All options mentioned above create a report file in either Perfetto format (Android 10 — API 29 or later) or Systrace format (earlier version of Android).
How to record TensorFlow Lite events
Before using any of the tools mentioned above we need to enable TensorFlow Lite tracing. The android system property
debug.tflite.trace
needs to be set to 1 before starting the android app. Use the following command to enable it.adb shell setprop debug.tflite.trace 1After we have recorded all trace events, we can disable it using following command.
adb shell setprop debug.tflite.trace 0Using Android CPU Profiler to record trace
Follow the steps below to start profiler and record trace in Android studio
Click Record and run the app for few seconds to do inference and then select stop
The Android CPU profiler parses the system trace and visualizes the trace data
The CPU profiler is improved over various updates of android studio and has very interesting features. The top timeline shows the usage of app over the recorded time. We can select any of the time range to inspect CPU and thread information using the box selector. we can zoom in and zoom out the timeline using the option on right corner of the window.
Here we are profiling the Image Classification example app from TensorFlow project to record trace and inspect. This example consists of two models EfficientNet and MobileNet. We will be profiling the EfficientNet model. In Android Studio, select the
inference
thread to understand the events happening. We can see the order of invocation of operators and their respective execution times.We can insert custom labels using the trace api to identify sections so that it is easy to distinguish. Here in the example two labels
recognizeImage
andrunInference
are added to identify the inference section. Following is how you can add these labels.... Trace.beginSection("runInference"); tflite.run(inputImageBuffer.getBuffer(), outputProbabilityBuffer.getBuffer().rewind()); Trace.endSection(); ...In the above example we have done inference on CPU in single thread. Following is the profile information of the app running on GPU.
In the
runInference
section we can seeTfLiteGpuDelegate
which means all operators have been delegated to GPU. For GPU we cannot see individual operator performance but if any of the operators of model graph are not delegated to GPU, they will be dispatched to CPU and the profiler will show run time information of such operators. To improve upon inference time for such models, we can replace such operators with those supported on GPU.Following is the profile data of MobileNet on GPU and we can see that operators
SOFTMAX
andSQUEEZE
are not supported by GPU and hence are executing on CPU.If you are interested, you can check here for the list of operators supported on GPU.
Sometimes we may be using multiple inference threads. These need to be scheduled with other CPU intensive tasks such as UI rendering and buffer management. Here the performance of inference is limited to the number of cores available. If the number of threads for inference is larger than available cores, then it will effect the performance. In such cases we either have to limit the number of threads or reschedule our CPU intensive tasks.
Summary
We have introduced on how to investigate performance of TfLite models on Android devices using the Trace Api. There are various tools to record system trace. Perfetto and Android CPU profiler are two popular tools with user friendly interface. We have explained how to use Android CPU profiler to record various types of information.
Profiling information such as operator invocation, graph modification by delegate can be analyzed using these tools which can help in improving the performance of model.
- 投稿日:2020-11-30T11:43:15+09:00
Identifying Performance bottlenecks in runtime of TensorFlow Lite models using Android Trace Api
In this article, we will introduce Android Tracing tools to capture internal events from the TensorFlow Lite interpreter which can help us identify potential issues in performance of the models.
This article assumes you know how to use Android studio to build and run apps on mobile devices.
Discussion Topics
- What is System Tracing in Android?
- Different tools to capture Trace
- How to record TensorFlow Lite events
- How to use the System Trace data
What is System Tracing in Android?
The method of recording all activity happening in a device over a short period of time is called System Tracing. Developers can use System Trace to troubleshoot the run time performance of app. It combines the data from Kernel, Android Framework, CPU scheduler, disk activity and app threads.
Current Android tracing tools support for combining the TensorFlow Lite Internal events alongside Java/Kotlin code events. Some example events that can be captured by this tool are tensor allocation, operator invocation, graph modification by delegate.
Tools to capture Trace
Android platform provides different options to capture traces.
Android Studio CPU Profiler
This is one of the popular tools to investigate system performance. It is integrated directly into the Android Studio with a rich interactive UI which provides real time visual information about the CPU usage and thread activity.
System Tracing App
This is a system level app that is present in devices running Android 9 (API level 28) or later. This records all events happening in the device using the systrace command-line utility without needing to connect over ADB. The trace file is saved over device and can be shared across teams.
Systrace command-line tool
This is a legacy tool to record a system trace over a short period of time and then compress it into a text file.
Perfetto command-line tool
Perfetto is an open-source tracing project which provides platform-wide tracing tools for Android, Linux, and Chrome. It is a more sophisticated tool and was introduced in Android 10.
CPU profiler and Perfetto provide a rich user-friendly UI to visualize the trace data, which can help in identifying the performance issues and the cause very easily. All options mentioned above create a report file in either Perfetto format (Android 10 — API 29 or later) or Systrace format (earlier version of Android).
How to record TensorFlow Lite events
Before using any of the tools mentioned above we need to enable TensorFlow Lite tracing. The android system property
debug.tflite.trace
needs to be set to 1 before starting the android app. Use the following command to enable it.adb shell setprop debug.tflite.trace 1After we have recorded all trace events, we can disable it using following command.
adb shell setprop debug.tflite.trace 0Using Android CPU Profiler to record trace
Follow the steps below to start profiler and record trace in Android studio
Click Record and run the app for few seconds to do inference and then select stop
The Android CPU profiler parses the system trace and visualizes the trace data
The CPU profiler is improved over various updates of android studio and has very interesting features. The top timeline shows the usage of the app over the recorded time. We can select any of the time ranges to inspect CPU and thread information using the box selector. We can zoom in and zoom out the timeline using the option on the right corner of the window.
Here we are profiling the Image Classification example app from the TensorFlow project to record trace and inspect. This example consists of two models EfficientNet and MobileNet. We will be profiling the EfficientNet model. In Android Studio, select the
inference
thread to understand the events happening. We can see the order of invocation of operators and their respective execution times.We can insert custom trace events using the trace api to identify sections so that it is easy to distinguish. In the example app two trace events
recognizeImage
andrunInference
are added to identify the inference section. Following is how you can add them.... Trace.beginSection("runInference"); tflite.run(...); Trace.endSection(); ...For all above examples we have done inference on CPU in a single thread. Following is the profile information of the app running on GPU.
In the
runInference
section we can seeTfLiteGpuDelegate
which represents graph modification for GPU delegate. For GPU we cannot see individual operator performance but if any of the operators of model graphs are not delegated to GPU, they will be dispatched to CPU and the profiler will show run time information of such operators. To improve upon inference time for such models, we can replace such operators with those supported on GPU.Following is the profile data of MobileNet on GPU and we can see that operators
SOFTMAX
andSQUEEZE
are not supported by GPU and hence are scheduled to CPU.If you are interested, you can check here for the list of operators supported on GPU.
Sometimes we may be using multiple inference threads. These need to be scheduled with other CPU intensive tasks such as UI rendering and buffer management. Here the performance of inference is limited to the number of cores available. If the number of threads for inference is larger than available cores, then it will affect the performance. In such cases we either have to limit the number of threads or reschedule our CPU intensive tasks.
Summary
We have introduced how to investigate performance of TfLite models on Android devices using the Trace Api. There are various tools to record system traces. Perfetto and Android CPU profiler are two popular tools with a user friendly interface. We have explained how to use Android CPU profilers to record various types of information.
Profiling information such as operator invocation, graph modification by delegate can be analyzed using these tools which can help in identifying potential issues and finding solutions to improve the performance of models.
- 投稿日:2020-11-30T11:22:20+09:00
MVVM+Repositoryのアーキテクチャを使っています②
この記事は
・前回(MVVM+Repositoryのアーキテクチャを使っています①)の続きです。
もくじ
今回はViewModelとRepositoryの部分について書いていきます。
②はViewModelから非同期処理でasync{}.await()
やwithContext()
を使ってRepositoryにある関数を呼び出します。Repositoryにはデータベース(今回はFirestore)にアクセスする処理が直接かいてあり、resume()
という関数で処理の結果を再びViewModelへ返します(⑤に当たる)。便宜上少しだけ⑥のRxRelayやLiveDataについても触れていきます。⑥の全体のコードや詳細はまた次回の記事に書かせてください。ViewModel(非同期処理:async/awaitを使うver)
まずRxRelayを使うための下準備をします。アプリレベルの
build.gradle
のdependencies{}
の中に以下を追加してください。build.gradle// RxJava RxRelay implementation 'io.reactivex.rxjava3:rxkotlin:3.0.0' implementation "io.reactivex.rxjava3:rxandroid:3.0.0" implementation 'com.jakewharton.rxrelay3:rxrelay:3.0.0'それではViewModelをみていきましょう。以下は、FirebaseのStorageにdebugフォルダを作り、その中のvoice.mp4に音声データを保存し、その音声のダウンロードURLを取得するコードです。参考:ファイルをアップロードする&ダウンロードURLを取得する
RecordPublishViewModel.ktclass RecordPublishViewModel : ViewModel() { // TIPS: repositoryから受け取った結果をActivity/Fragmentへ引き渡すためのRxRelayを用意 val storageUploadSuccess: PublishRelay<StorageSuccess> = PublishRelay.create() data class StorageSuccess( val data: String ) fun uploadVoiceFile(fileName: String, repository: StorageRepository = StorageRepository()) { viewModelScope.launch { try { val firebasePath = "debug/voice.mp4" // TIPS: async/awaitで非同期処理。repositoryにあるupload()という関数を呼び出しています val task = async { repository.upload(fileName, firebasePath) }.await() if (task.isSuccessful) { val result = StorageSuccess(task.result.toString()) // TIPS: accept()でrepositoryからの結果(result)を受け取り、Activity/Fragmentへ引き渡す storageUploadSuccess.accept(result) } } catch (e: Exception) { Log.d("ERROR", e.toString()) } } }また次回の記事で詳細を書きますが、RxRelayの
accept()
で受取った値はAvtivity/Fragmentで以下のようにsubscribe{}
を利用して受け継げます。RecordPublishActivity// TIPS: subscribeで結果(音声のダウンロードURL)を受け取れる viewModel.storageUploadSuccess.subscribe { postUrl = it.data Log.d("TAG", postURL.toString()) // ダウンロードURLが取得できる }Repository
StorageRepositoryclass StorageRepository { private val storage = Firebase.storage // TIPS: firebaseのstorageへ音声データを保存してダウンロードURLを取得 suspend fun upload(fileName: String, path: String) : Task<Uri> { return suspendCoroutine { cont -> val storageRef = storage.reference val stream = File(fileName).inputStream() val ref = storageRef.child(path) ref.putStream(stream).continueWithTask { task -> if (!task.isSuccessful) { task.exception?.let { throw it } } // TIPS: ダウンロードURLを取得 ref.downloadUrl }.addOnCompleteListener { // TIPS: resume()で結果をViewModelへわたす cont.resume(it) } } } }上記が、RxRelayやasync/awaitを使うViewModelとRepositoryのやりとりです。
ちなみに別にRxRelayとasync/awaitは必ずセットで使わないといけないというわけでもないと思います。あくまでRxRelayとasync/awaitの説明をただ一緒のところに書いただけと思ってください。ViewModel(非同期処理:withContext()を使うver)
このコードでは、非同期処理部分を
async{}.await()
のかわりにwithContext()
を使っております。また、Repositoryで得た結果をActivity/Fargmentへ伝えるのにはRxRelayではなくLiveDataを使用しております(参考:LiveDataの実装)。コードの内容としては、Firestoreに保存したデータを日付降順で取得するというプログラムです。HomeViewModelclass HomeViewModel : ViewModel() { // TIPS: LiveDataを使用してrepositoryから受け取ったデータをActivity/Fragmetに渡す private val _postsData = MutableLiveData<List<Post>>() val postsData: LiveData<List<Post>> = _postsData private val repository = LoadPostsRepository() fun loadPost() = viewModelScope.launch { try { // TIPS: withContext()で非同期処理 // TIPS: repoitoryのload()という関数を呼び出している val posts = withContext(Dispatchers.Default) { repository.load() } // TIPS: LiveDataに結果を渡す _postsData.value = posts } catch (e: Exception) { Log.d("ERROR", e.toString()) } } }上の
async{}.await()
で書いてみたバージョンを試してみると分かりますが、黄色の波線が現れて、『asyncを余分に使っていて、withContextを使うと解消できますよ』と表示されます。その表示の少し下にmerge call chain to withContext
と青く書かれた部分があるのでそこをクリックすると自動でasync{}.await()
をwithContext()
にしてくれます。async{}.await()
も使えますが、このコードの場合はwithContext()
の方がきれいなのかもしれません。では以下で、ViewModelでLiveDataを使った場合のRepositoryをみていきましょう。
LoadPostsRepositoryclass LoadPostsRepository { suspend fun load(): List<Post> { return suspendCoroutine { cont -> val db = FirebaseFirestore.getInstance() // TIPS: "posts"はアクセスしたいFirestoreのコレクション名です val task = db.collection("posts") // TIPS: このPostはDataClassの名前です .orderBy(Post::createdAt.name, Query.Direction.DESCENDING) .limit(20) .get() task.addOnCompleteListener { val resultList = task.result.toObjects(Post::class.java) // resume()で結果をViewModelへ渡します。 cont.resume(resultList) } } } }参考:AndroidでFirebaseのCloudFirestoreを使ってみた(Kotlin)
RxRelayでもLiveDataでもrepositoryの書き方は変わりません。どちらもresume()
でFirebaseにアクセスした結果をViewModelに渡しています。また次回以降詳しくかきますが、LiveDataをつかった場合Activity/Fragmentでは以下のように
observe()
を使ってデータを受け取ります。HomeFragmentoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) callHomeViewMode() viewModel.run { postsData.observe( viewLifecycleOwner, { homeListAdapter.submitList(it) } ) } } private fun callHomeViewMode() { viewModel.loadPost() }上記のコードは
observe()
で受取ったデータを、submitList()
をつかってListAdapterに送っています。うけとったデータ(List)をRecyclerViewに表示するためです。まとめ
・ViewModelからRepositoryへアクセスするのは非同期処理で
async{}.await()
やwithContext()
が使えます。
・Repositoryでの結果をViewModelで受取り、さらにそれをActivity/Fragmentに伝えるのにはRxRelayのaccept()
・subscribe{}
を利用したり、LiveDataとobserve()
を利用することもできます。間違っているところ等あればご指摘よろしくお願い致します!!!
- 投稿日:2020-11-30T11:20:02+09:00
TabLayoutのIndicator色をtabごとに設定
setSelectedTabIndicatorColor
が非推奨になったので、addOnTabSelectedListener
を使用。MainActivity.kttab_layout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabReselected(p0: TabLayout.Tab?) {} override fun onTabUnselected(p0: TabLayout.Tab?) {} override fun onTabSelected(p0: TabLayout.Tab?) { tab_layout.setSelectedTabIndicatorColor( ContextCompat.getColor( applicationContext, when (p0?.position) { 0 -> R.color.red 1 -> R.color.blue else -> R.color.green } ) ) } })
- 投稿日:2020-11-30T11:20:02+09:00
【Android】TabLayoutのIndicator色をtabごとに設定
setSelectedTabIndicatorColor
が非推奨になったので、addOnTabSelectedListener
を使用。MainActivity.kttab_layout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabReselected(p0: TabLayout.Tab?) {} override fun onTabUnselected(p0: TabLayout.Tab?) {} override fun onTabSelected(p0: TabLayout.Tab?) { tab_layout.setSelectedTabIndicatorColor( ContextCompat.getColor( applicationContext, when (p0?.position) { 0 -> R.color.red 1 -> R.color.blue else -> R.color.green } ) ) } })
- 投稿日:2020-11-30T05:19:46+09:00
React Nativeのメリット・デメリット
はじめに
先日、React Nativeを使った個人開発アプリをリリースしました。
そこで、この記事ではReact Nativeでアプリを開発してみて良かった点、良くなかった点を紹介したいと思います。「React Nativeを始めようかな」と考えている人の役に立つことができれば嬉しいです。
メリット❶ 学習コストが少ない
1つ目のメリットは学習コストが少なかったことです。
私は今回のアプリを開発する前は、JavaScriptのライブラリであるReactでWebアプリケーションの開発を行っていました。
React Nativeはコンポーネントの概念などReactと共通している部分が多いので、非常に開発に取り掛かりやすかったです。
Reactを使用してWebアプリケーションの開発を行っている方であれば、React Nativeの学習は参入障壁が非常に低く、取り組みやすいと感じます。メリット❷ コードの修正が簡単
2つ目のメリットはコードの修正が簡単なことです。React Nativeは、ホットリロード機能があるため、自分が書いたコードを保存すれば、自動でリロードしてくれます。
例えば、UIを修正する際は変更したコードが瞬時に画面に反映されるため、スムーズに開発を進めることが出来ます。メリット❸ 開発が効率的
3つ目のメリットは開発が効率的になることです。React Nativeは、クロスプラットフォームフレームワークであるため、iOSとAndroidのアプリを同時に開発することができます。この点はReact Nativeの大きな魅力であると感じました。
デメリット❶ エラーの解決や機能の実装に時間がかかる
続いてデメリットの紹介です。
1つ目のデメリットはエラーの解決や機能の実装に時間がかかることです。
個人的にReact Nativeは、エラーの解決や新しい機能を実装したいときに参考にするネット記事や資料が、他言語に比べて少ないと感じました。
そのため、アプリ開発中は英語の記事や資料を参考にする必要がありました。デメリット❷ネイティブエンジニアには学習コストが高い
普段からネイティブ言語で開発しているエンジニアにとっては、1からJavaScriptを学習しなければいけないため、学習コストが高くなると考えられます。
まとめ
以上のように今回の記事では、私がReact Nativeでアプリを開発して感じたメリット・デメリットについて紹介しました。Reactの知識を使って、モバイルアプリケーションを開発できるという点が非常に素晴らしいと感じています。
ちなみに、今回私がリリースしたアプリはこちら
次回の記事では、実際に開発したアプリについて紹介してみようと思います!