Xamarin.Forms+AVFoundationでEANバーコードの読み込み2(キャプチャ停止コード/ユーザ応答実装編)

Xamarin.Forms+AVFoundationでEANバーコードの読み込み(実装編)」の続きです。VisualStudio for Mac+iOSで作業しています。

Xamarin+AVFoundationでメタデータを利用する時に利用する「DidOutputMetadataObjects」メソッドは、対象にカメラを向けるだけで呼び出されるので、値を取得してカメラキャプチャを続けたくない時は、キャプチャ停止コードを追加してあげる必要があります。

AVCaptureMetadataOutputObjectsDelegateを継承したクラスと、実際のカメラキャプチャON/OFFを操作するクラスは別になっている時の、カメラ認識→自動キャプチャ停止を行う場合の1例として。

1.DidOutputMetadataObjectsからの通知(実装コード)

public class OutputRecorder  : AVCaptureMetadataOutputObjectsDelegate
{
    public override void DidOutputMetadataObjects(
        AVCaptureMetadataOutput _Output,
        AVMetadataObject[] _MetaObject,
        AVCaptureConnection _Connection)
        {
            var StackReadStr = new List<string>();
            foreach (var data in _MetaObject)
            {
                var metaData = (AVMetadataMachineReadableCodeObject)data;
               
                if (metaData.Type == AVMetadataObjectType.EAN13Code)
                {
                    Console.WriteLine($"{metaData.StringValue}");
                    if (!StackReadStr.Contains(metaData.StringValue))
                        StackReadStr.Add(metaData.StringValue);
                }
            }
            // EANバーコードを読み取っていた場合にイベント発行
            if (StackReadStr.Count > 0)
            {
                var args = new BarcodeCaptureEventArgs {
                    EAN13Code = StackReadStr,
                    CancelRequested = false
                };
                _ = Task.Run(()=> OnBarcodeCaptured(args));
           }
       }

       protected virtual void OnBarcodeCaptured(BarcodeCaptureEventArgs e)
       {
            EventHandler<BarcodeCaptureEventArgs> handler = BarcodeCaptured;
            handler?.Invoke(this, e);
       }

       public event EventHandler<BarcodeCaptureEventArgs> BarcodeCaptured;
   }

停止制御を行う場合は、カメラキャプチャON/OFFする側に「ISBNコード受け取ったよ。値はこれだよ。」って伝えてあげる必要があります。

実装方法として思い付くのは以下の2つ。
a.値をタイマーで監視し、値の変更をトリガーに処理を実行
b.c#のイベントを発行し、イベントをトリガーに処理を実行

実装コードは「b.」の方法を採用。
AVCaptureMetadataOutputObjectsDelegateを継承したクラスをインスタンス化した際に、BarcodeCapturedへデリゲートメソッドを登録しておき、実際の実行は「_ = Task.Run(()=> OnBarcodeCaptured(args));」部分を利用します。

(別にTask.Runである必要はないですが、同期実行してしまうと、このOnBarcodeCapturedの先で非同期メソッドが実行される、もしくは、全処理が終わるまで先に進まないので。)

2.今回共通で利用しているBarcodeCaptureEventArgs

public class BarcodeCaptureEventArgs : EventArgs
{
   public List<string> EAN13Code { get; set; }
   public bool CancelRequested { get; set; }
}

イベントとして通知する内容は、バーコードを格納したList<string>と、カメラキャプチャ制御用のbool変数。
カメラキャプチャした値を利用するだけで、停止する/しないの判断とかがなければCancelRequestedは不要です。

3.呼び出し側のデリゲートメソッドの登録(カスタムレンダラーへの実装)

protected override void OnElementChanged(ElementChangedEventArgs<CameraPreview> e)
{
	base.OnElementChanged(e);

	if (e.OldElement != null)
	{
		// Unsubscribe
		uiCameraPreview.Tapped -= OnCameraPreviewTapped;
	}
	if (e.NewElement != null)
	{
		if (Control == null)
		{
			uiCameraPreview = new UICameraPreview(e.NewElement.Camera);
			SetNativeControl(uiCameraPreview);
		}
		// Subscribe
		uiCameraPreview.Tapped += OnCameraPreviewTapped;
		uiCameraPreview.CAControl.Recorder.BarcodeCaptured += Noticed;
	}
}

「uiCameraPreview.CAControl.Recorder.BarcodeCaptured += Noticed;」の箇所でデリゲートメソッドを登録しています。

void Noticed(object sender, BarcodeCaptureEventArgs e)
{
    uiCameraPreview.CaptureSession.StopRunning();
	uiCameraPreview.IsPreviewing = false;
	Element.BarcodeConfirm(e);

	if (e.CancelRequested==true)
    {
		uiCameraPreview.CaptureSession.StartRunning();
		uiCameraPreview.IsPreviewing = true;
        Console.WriteLine($"Push Cancel bottom. Capture Restart!");
    }
}

登録されたデリゲートメソッドの中身。

ひとまず値の評価をするので、「uiCameraPreview.CaptureSession.StopRunning();」でキャプチャを止め、「Element.BarcodeConfirm(e);」でISBNコードの評価とユーザの応答を実装。

デリゲートメソッドの実装はXamarin.Formsのカスタムレンダラーにしたので、ElementはカスタムレンダラーのXamarin.Forms側のインスタンスを指します。

「Element.BarcodeConfirm(e);」でISBNコードが正しいものである場合は、カメラキャプチャOFFのままとし、そうでない場合(確認した結果、やり直しとなった場合)はEventArgsの「CancelRequested」がTureになっているので、「uiCameraPreview.CaptureSession.StartRunning();」でカメラキャプチャをリスタートさせます。

4.ユーザ応答部分のコード抜粋(ページを表示しているView.xaml.csへの実装)

Device.InvokeOnMainThreadAsync(async() => {

   var userControl = await DisplayAlert(
       "Information",
       "ISBN Code : " + str,
       "Cancel",
       "Accept");

   if (userControl)
   {
       e.CancelRequested = true;
       return;
   }

}).Wait();

Xamarin.Formsのコントロールを操作したり、ユーザ通知用のDisplayAlertを利用する場合、UIを生成したメインスレッドに戻す必要があります。

DidOutputMetadataObjectsメソッドが実行された時点で、UIを生成したメインスレッドではないはずなので、「Device.InvokeOnMainThreadAsync」を使ってあげないとXamarin.Formsコントロールにアクセスした段階で、アプリが止まります。

さらに、EventArgsのCancelRequestedを呼び出し元で参照したいので、ここについては非同期で実行させるとNoticedメソッドで値の評価ができません。このため、Device.InvokeOnMainThreadAsyncの最後に「Wait();」を入れています。

5.メインスレッドで実行させるメソッドの補足

UIを生成したメインスレッドで実行させるための手段として、「Device.InvokeOnMainThreadAsync」と「Device.BeginInvokeOnMainThread」が用意されていますが、ドキュメント上は以下のように記載されています。

デバイスのメイン (UI) スレッドでアクションを呼び出します。

もうちょっと説明しても良いんじゃないですかね……。

違いはTaskが戻されるかどうかぐらいっぽいので、同期を待ちたい場合は、「Device.InvokeOnMainThreadAsync」の方を利用します。

Xamarin.FormsにはXamarin.Essentialsの中にも、メイン実行スレッドで実行させることが出来るメソッドがあります。

判断基準は、以下の引用部分の通り。

Xamarin.Forms には Device.BeginInvokeOnMainThread(Action) と呼ばれるメソッドがあります。 これは MainThread.BeginInvokeOnMainThread(Action) と同じ処理を行います。 Xamarin.Forms アプリではどちらのメソッドも使用できますが、呼び出し元のコードが他にも Xamarin.Forms に依存する必要があるかどうかを検討してください。 ない場合は、MainThread.BeginInvokeOnMainThread(Action) が適切な選択であると考えられます。

Formsのコントロール、DisplayAlert系はXamarin.Formsに依存しているはずなので、この場合はDevice〜系が正しいはず。

6.実行結果イメージ

Xamarin_実行イメージ

ISBNコードを取得した上で応答が出来ました(EANコードはAlertの裏側なので隠れてますが)。
ISBNの文字列とハイフンなしで一致しています。


いいなと思ったら応援しよう!