- 投稿日:2020-09-21T19:00:56+09:00
ContextからStartActivityする際の注意
以前KudanARライブラリを使用したKudanARアプリをリリースしました。
Xamarin.FormsでKudan ARを試してみるそのアプリのテスト環境として使用していたAndroid8.0では発生しなかったが、Android9.0だと発生するエラーがありました。
少し調査したので軽くまとめたいと思います。先に結論
Contextの実体には
Application Context
、Activity Context
があります。
実体を把握した上で使用しよう。現象
まずは、下記が確認できたエラーメッセージの内容です。
Android.Util.AndroidRuntimeException: 'Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?'どうやら下記の
StartActivity
実行時にエラーが発生していることがわかりました。// Androidバージョンによってはエラーが発生する書き方 var context = Android.App.Application.Context; using (var intent = new Android.Content.Intent(context, typeof(MarkerARActivity))) { context.StartActivity(intent); }原因
Contextと一言に言っても、実体が異なる場合があることを把握しないまま使用したことが原因です。
なぜアプリリリース前に気づけなかった?
KudanARライブラリを使用すると、エミュレータで動作確認ができなくなる仕様があります。
また、私が持っている一番新しいAndroidバージョンは8.0です。
よって、他のAndroidバージョンでの挙動を確認できなかったため、リリース後に気づいた次第です。
言い訳終了。エラー対処方法
Xamarinのテンプレートソリューションから、空のActivityを呼び出すだけのアプリを作成し、動作を確認しました。
対処方法にはいくつかあるようです。
context = Android.App.Application.Context;
を使う場合
- Contextの実体は
Application
- Intentにフラグを追加して対応
intent.SetFlags(ActivityFlags.NewTask);
context = (Context)Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity;
を使う場合
- Contextの実体は
Activity
- NuGetから
Plugin.CurrentActivity
をインストールする必要あり- Intentへのフラグ追加は必要なし
context = MainActivity.Context;
を使う場合
- Contextの実体は
Activity
public static Context Context { get; private set; }
のような適当な静的プロパティをMainActivityに用意する必要あり(OnCreateでthisの代入を行っておく)- Intentへのフラグ追加は必要なし
下記の記事が参考になりました。
Androidの勉強:Contextについて最終的な実装
フラグ設定処理を追加して行数を増やすのは避けたかったので、対処方法の2番目に挙げた方法で実装しました。
// 正常に動作する書き方 var context = (Context)Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity; using (var intent = new Android.Content.Intent(context, typeof(MarkerARActivity))) { context.StartActivity(intent); }まとめ
Contextの実体には
Application Context
、Activity Context
があるので、実体を把握した上で使用しよう。
Contextの取得には、Plugin.CurrentActivity
を使用するのが安定。
- 投稿日:2020-09-21T17:40:16+09:00
【Unity(C#)】ファイルの保存上限を設けて日付の古いファイルから順に消去する実装
はじめに
【参考リンク】:【Unity(C#)】テクスチャを画像データに変換して端末に保存、読み込み
↑前回の応用です。やりたい事としては下記の画像のように
ファイルの保存上限を設けて、
上限を超えた場合は最も古いファイルから削除するという実装です。
デモ
早速ですがデモです。
保存するファイルは画像ファイルとしました。
PC内の保存領域を確認したところ、
しっかりと最後の3枚が保存されていました。コード
下記今回の処理の全文です。
適当なオブジェクトにアタッチusing System; using System.Collections.Generic; using System.IO; using System.Linq; using UniRx; using UniRx.Triggers; using UnityEngine; using UnityEngine.UI; /// <summary> /// テクスチャー ⇔ Png画像 の変換と保存と読み込み /// </summary> public class TexturePngConverter : MonoBehaviour { [SerializeField] private Button _saveButton; [SerializeField] private Button _loadButton; [SerializeField] private Image _paintImage; [SerializeField] private Painter _painter; [SerializeField] private Transform _loadImagesParentTransform; private const string IMAGE_SAVE_FOLDER = "Image"; private const string PNG = ".png"; /// <summary> /// 限界保存枚数 /// </summary> private const int UPPER_LIMIT_SAVE_PICTURE = 3; private void Start() { //セーブボタン _saveButton.OnPointerClickAsObservable() .Subscribe(_ => { //保存 ConvertToPngAndSave(GetSaveDirectoryPath(IMAGE_SAVE_FOLDER), GetSaveFilePath(IMAGE_SAVE_FOLDER, PNG)); //リセット _painter.ResetTexture(); }).AddTo(this); //ロードボタン _loadButton.OnPointerClickAsObservable() .Subscribe(_ => ConvertToTextureAndLoad(GetSaveDirectoryPath(IMAGE_SAVE_FOLDER))).AddTo(this); } /// <summary> /// 保存先のファイルのパス取得 /// </summary> /// <param name="folderName">区切りのフォルダ名</param> /// <param name="fileType">拡張子</param> /// <returns>保存先のパス</returns> private string GetSaveFilePath(string folderName, string fileType) { return GetSaveDirectoryPath(folderName) + DateTime.Now.ToString("yyyyMMddHHmmss") + fileType; } /// 保存先のディレクトリのパス取得 /// </summary> /// <param name="folderName">区切りのフォルダ名</param> /// <returns>保存先のパス</returns> private string GetSaveDirectoryPath(string folderName) { string directoryPath = Application.persistentDataPath + "/" + folderName + "/"; if (!Directory.Exists(directoryPath)) { //まだ存在してなかったら作成 Directory.CreateDirectory(directoryPath); return directoryPath; } return directoryPath; } /// <summary> /// "ディレクトリ配下のファイル"が全て入ったリストを返す /// 最も古いファイルが[0]番目 /// </summary> /// <param name="directoryName">取得したいファイル群の親ディレクトリ</param> /// <returns>指定したディレクトリ配下のファイルが全て入ったリスト</returns> private List<string> GetAllFileFromDirectory(string directoryName) { //古いものが先頭にくるようにファイルをソート List<string> imageFilePathList = Directory //Imageディレクトリ内の全ファイルを取得 .GetFiles(directoryName, "*", SearchOption.AllDirectories) //.DS_Storeは除く .Where(filePath => Path.GetFileName(filePath) != ".DS_Store") //日付順に降順でソート .OrderBy(filePath => File.GetLastWriteTime(filePath).Date) //同じ日付内で時刻順に降順でソート .ThenBy(filePath => File.GetLastWriteTime(filePath).TimeOfDay) .ToList(); return imageFilePathList; } /// <summary> /// 画像に変換&保存 /// 上限保存数をチェック /// </summary> /// <param name="directoryPath">保存数をチェックするディレクトリ</param> /// <param name="fileSavePath">保存先のパス</param> private void ConvertToPngAndSave(string directoryPath, string fileSavePath) { //指定したディレクトリー配下のファイルが全て入ったリストを取得 List<string> imageFilePaths = GetAllFileFromDirectory(directoryPath); //ファイル数の上限をチェック if (imageFilePaths.Count >= UPPER_LIMIT_SAVE_PICTURE) { //上限に達していた場合、最も古いファイルを削除 File.Delete(imageFilePaths[0]); } //Pngに変換 byte[] bytes = _paintImage.sprite.texture.EncodeToPNG(); //保存 File.WriteAllBytes(fileSavePath, bytes); } /// <summary> /// テクスチャに変換&読み込み /// </summary> /// <param name="directoryPath">ロードしたいファイル群の親ディレクトリ</param> private void ConvertToTextureAndLoad(string directoryPath) { List<Image> imageList = new List<Image>(); //ロード後、複数枚表示するためのImageリスト作成 foreach (Transform child in _loadImagesParentTransform) { Image childImage = child.gameObject.GetComponent<Image>(); if (childImage != null) { imageList.Add(childImage); } } //指定したディレクトリー配下のファイルが全て入ったリストを取得 List<string> imageFilePaths = GetAllFileFromDirectory(directoryPath); //インデックス用カウンター int count = 0; //ファイルのリストから古い順にロードしてImageに適用 foreach (string imageFilePath in imageFilePaths) { Debug.Log(imageFilePath); //読み込み byte[] bytes = File.ReadAllBytes(imageFilePath); //画像をテクスチャに変換 Texture2D loadTexture = new Texture2D(2, 2); loadTexture.LoadImage(bytes); //テクスチャをスプライトに変換 imageList[count].sprite = Sprite.Create(loadTexture, new Rect(0, 0, loadTexture.width, loadTexture.height), Vector2.zero); //インデックス用カウンターを進める count++; } } }ソート
今回は古いフォルダの概念を保存した日時(秒単位)にしています。
そのため、ディレクトリ内のファイルの中から
最も古い保存日時のファイルを取得する必要がありました。そのために一度全ファイルを取得して
それぞれの保存先のパスを保存順にソートしています。ロード時も同様に全ファイルのパスを取得し
File.ReadAllBytes
の引数に渡しています。下記リンクを参考にLINQを使って簡単に書くことができました。
【参考リンク】:タイムスタンプ(作成日、変更日、最後に開いた日)を基準にファイルを古い順や新しい順にソート【C#】【LINQ】
DateTime.Now
DateTime.Now
を使えば現在の日付と秒単位までの時刻を取得することができます。今回の実装においては、
この日付と秒単位までの時刻をファイル名とすることで
ユニークなパスを生成しています。ただ、そのままファイル名として与えた際に少し厄介なことがありました。
Image/2020/09/21 17:12:43.png
のようにディレクトリの区切りとして解釈される
/(スラッシュ)
が含まれてしまいます。このまま保存処理を行おうとすると
そんなディレクトリは存在しませんと怒られます。ですので、下記のように引数に
"yyyyMMddHHmmss"
を渡しています。DateTime.Now.ToString("yyyyMMddHHmmss")こうすることで
Image/20200921171710.png
となり
秒数まで全て数字で取得することが可能となります。【参考リンク】:日付や時刻を文字列に変換するには?
2020/09/21 追記
GetFiles(directoryName, "*", SearchOption.AllDirectories)
の箇所を"*"
→"*.png"
等にした方が
想定外のファイルを除外することができるとコメント頂いたので
メモしときます (ありがとうございます!)おわりに
モバイル端末で一気に5,60枚読み込んでも処理落ちは見られませんでした。
枚数が何百、何千となるといろいろと工夫が必要そうです。
- 投稿日:2020-09-21T17:20:10+09:00
C#でUDPブロードキャストパケット受信
参考記事
https://garafu.blogspot.com/2015/08/broadcast.html
実装
UDPブロードキャストパケットを受信してエコーするタスクを常駐させるサンプルです。
using System.Net; using System.Net.Sockets; namespace udp { public class udpSample { public void start() { Task.Run(() => BroadcastReceiver()); // UDPブロードキャストパケット受信タスク起動 } private void BroadcastReceiver() { var endPoint = new IPEndPoint(IPAddress.Any, 4000); // すべてのアドレス監視するためIPAddress.Anyを指定 using (UdpClient udpClient = new UdpClient(endPoint)) { while(true) { var buff = udpClient.Receive(ref endPoint); // 受信(同期) udpClient.Send(buff, buff.Length, endPoint); // 受信内容をエコー } } } } }IPEndPointに
using System.Net;
が、
UdpClientにusing System.Net.Sockets;
が必要になります。
- 投稿日:2020-09-21T11:44:06+09:00
C#のLambdaプロジェクトを作成してVisual Studio for Macでデバッグ実行する
はじめに
MacでC#のLambdaプロジェクトを作成し、Visual Studio for Macでテストプロジェクトをデバッグ実行した記録です。
できたこと
- C#のLambdaプロジェクトを作成
- AWS Lambdaへのデプロイ
- AWS Lambdaでのテスト
- Visual Studio for Macでソリューション化
- テストプロジェクトのデバッグ実行
AWS Lambda上で動くプログラムをアタッチしてデバッグしたわけではありません。
テストプロジェクトをローカルPC上でデバッグ実行しました。準備
アクセスキー ID とシークレットアクセスキーを設定
PCからAWSにアクセスできるようにアクセスキー ID とシークレットアクセスキーを設定します。
AWS CLIのインストール
インストール手順は以下のURLを参照してください。
参考:https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-install.html設定
以下のURLを参考にアクセスキー ID とシークレットアクセスキーを設定します。
参考:https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-quickstart.html.NET Core CLI
プロジェクトテンプレートの追加
dotnet new -i Amazon.Lambda.Templates
コマンドを実行して、.NET Core CLIにAWS Lambdaのテンプレートを追加します。Amazon.Lambda.Tools .NET Core Global Tool のインストール
dotnet tool install -g Amazon.Lambda.Tools
コマンドを実行します。参考:https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/csharp-package-cli.html
プロジェクトの作成
空のLambdaプロジェクト作成するコマンドを実行します。
dotnet new lambda.EmptyFunction --name {メソッド名}
(以下、{メソッド名}に「SelectFunction」と指定した想定で説明します。)色々とファイルが生成されますが、Lambda関数を実行するとFunction.csのFunction.FunctionHandler()メソッドが呼び出されることになります。
デフォルトでは、引数inputで受け取った文字列を大文字に変換して戻り値に返す処理となっています。Function.csusing Amazon.Lambda.Core; // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] namespace SelectFunction { public class Function { /// <summary> /// A simple function that takes a string and does a ToUpper /// </summary> /// <param name="input"></param> /// <param name="context"></param> /// <returns></returns> public string FunctionHandler(string input, ILambdaContext context) { return input?.ToUpper(); } } }参考:https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/csharp-package-cli.html
デプロイ
デプロイしてみます。
- .csprojファイルがあるディレクトリに移動します。
dotnet restore
コマンドを実行します。
dotnet lambda deploy-function
を実行します。
Enter AWS Region:
と表示されるので、デプロイ先のリージョンを入力。Enter Function Name:
と表示されるので、作成したいLambda関数名を入力。Select IAM Role that to provide AWS credentials to your code:
とIAMロールの一覧が表示されるので、使用するIAMロールを選択。デプロイに成功すると、AWS Lambdaのコンソールに作成したLambda関数が表示されます。
AWS Lambda上でテスト
AWS LambdaコンソールでデプロイしたLambda関数を選択し、「テストイベントの設定」をクリックします。
「イベント名」欄に任意の名称を入力し、その下の編集エリアに引数に渡す情報をJsonで入力して保存ボタンをクリックします。
先ほど「イベント名」欄に入力した名前を選択してテストボタンをクリックすると、実行結果欄にFunction.csのFunction.FunctionHandler()メソッドの戻り値が表示されます。
Visual Studio for Macでソリューション化
Visual Studio for Macでコーディングやデバッグ実行をしたいのでソリューションを作成します。
まずは、空のソリューションを作成します。プロジェクトの作成で作成されたディレクトリ構成に合わせてソリューションの場所を指定します。ディレクトリ構成はお好きにどうぞ。
ソリューションウィンドウでソリューション名を右クリック→[追加]→[既存のプロジェクト]を選択し、Lambdaプロジェクトとそのテストプロジェクトを追加します。
テストの実行
テストプロジェクトをスタートアッププロジェクトに設定します。
単体テストウィンドウを表示し、テストメソッドを右クリック→[テストのデバッグ]をクリックすると、テストメソッドをデバッグ実行できます。
- 投稿日:2020-09-21T00:01:07+09:00
【Unity(C#)】Lerpで滑らかな線を描く
はじめに
平面上に線を滑らかに描く方法を調べました。
VR空間でもぬるぬる動いている例を参考にしました(↓すごい)
【参考リンク】:ホワイトボードで線がヌルヌル描ける仕組み
描く実装
"描く部分"のコードは下記リンクからまるまる拝借しました。
【参考リンク】:Unityでテクスチャにお絵描きしよう
デモ
線がかすれてしまって不格好です。
しっかりと線の間を補間してくれました。
コード
下記全文です。
using UnityEngine; //使い方↓ //https://nn-hokuson.hatenablog.com/entry/2016/12/08/200133 public class SmoothPaint : MonoBehaviour { Texture2D drawTexture; Color[] buffer; private Vector2 _prevPosition; void Start() { Texture2D mainTexture = (Texture2D) GetComponent<Renderer>().material.mainTexture; Color[] pixels = mainTexture.GetPixels(); buffer = new Color[pixels.Length]; pixels.CopyTo(buffer, 0); drawTexture = new Texture2D(mainTexture.width, mainTexture.height, TextureFormat.RGBA32, false); drawTexture.filterMode = FilterMode.Point; } public void Draw(Vector2 p) { for (int x = 0; x < 256; x++) { for (int y = 0; y < 256; y++) { if ((p - new Vector2(x, y)).magnitude < 5) { buffer.SetValue(Color.black, x + 256 * y); } } } } void Update() { if (Input.GetMouseButton(0)) { //前回値がまだないなら現在の値を前回値として扱う if (_prevPosition == Vector2.zero) { _prevPosition = Input.mousePosition; } //線形補間に使う入力の終点座標 Vector2 endPosition = Input.mousePosition; //1フレームの線の距離 float lineLength = Vector2.Distance(_prevPosition, endPosition); //線の長さに応じて変わる補間値 CeilToIntは小数点以下を切り上げ int lerpCountAdjustNum = 5; int lerpCount = Mathf.CeilToInt(lineLength / lerpCountAdjustNum); for (int i = 1; i <= lerpCount; i++) { //Lerpの割合値を "現在の回数/合計回数" で出す float lerpWeight = (float) i / lerpCount; //前回の入力座標、現在の入力座標、割合を渡して補間する座標を算出 Vector3 lerpPosition = Vector2.Lerp(_prevPosition, Input.mousePosition, lerpWeight); Ray ray = Camera.main.ScreenPointToRay(lerpPosition); RaycastHit hit; if (Physics.Raycast(ray, out hit, 100.0f)) { Draw(hit.textureCoord * 256); } drawTexture.SetPixels(buffer); drawTexture.Apply(); GetComponent<Renderer>().material.mainTexture = drawTexture; } //前回の入力座標を記録 _prevPosition = Input.mousePosition; } else { //前回の入力座標をリセット _prevPosition = Vector2.zero; } } }ロジックとしては下記です
①前フレームのマウスの入力座標を保持
②次フレームにて①と現在の入力座標の距離を算出
③距離に応じて補間値(補間回数)を算出
④補間値を利用しLerpで補間線が繋がる
実装していてそこそこ時間を使ってしまったのが、
線が繋がってしまう という現象でした。下記GIFのように新しく描き始めた箇所と
前回の終了地点が繋がってしまっていました。これは描き終えた際の値を前回値として保持したまま
新しい入力箇所での補間を行っていることが原因でした。ですので、下記箇所で入力が無い状態になった際に前回値のリセットを行っています。
さらに、入力値のリセットにより前回値が存在しないフレーム、
すなわち描き始めのフレームにおいては補間を行う必要が無いので
前回の値=現在の値として扱うようにしています。if(Input.GetMouseButton(0)) { //前回値がまだないなら現在の値を前回値として扱う if (_prevPosition == Vector2.zero) { _prevPosition = Input.mousePosition; } } else { //前回の入力座標をリセット _prevPosition = Vector2.zero; }これにより描き始めた箇所に線を新しく描画することができました。
おわりに
線の太さ(大きさ)も考慮した補間値を算出すれば
もっと最適な補間ができるみたいです。そのうち書けたら書きます。