[Xamarin.Forms]画像を選択して表示する覚書


## はじめに

Xamarin.FormsはiOSとAndroidの最大公約数的な機能しかないので、画像を選択して表示するだけでも簡単にはいかないようだ。
とりあえずの実現方法を模索してみた。

## 方針

DependencyServiceを使い、実際の処理は各OSにまかせる。

## 共通部分

DependencyServiceで使うInterfaceを定義する。

```csharp:IImageService.cs
using System;
using Xamarin.Forms;

namespace ImageViewer.Services {
    public interface IImageService {
        void ShowImageGallery(Action<ImageSource> imageSelectedHandler);
    }
}
```

呼び出し方は以下のような感じ。
ここでは、ハンドラーを渡して選択結果をImageSourceに変換したものを受け取る形にした。

>ImageSourceで受け取ると、割と使いやすい気がします。
>変換するのもそこまで複雑なコードにならないし、受け取った側もXaml中で```<Image Source={Binding imageSource} />```みたいに使えるので使いやすい。

```csharp:MainPageViewModel.cs
    DependencyService.Get<IImageService>().ShowImageGallery((imageSoruce) => {
        this.ImageSource = imageSoruce;
    });
```



## iOS

iOSでのIImageServiceの実装は以下のようになる。

```iOS/Services/ImageService.cs
using System;
using Xamarin.Forms;
using UIKit;
using ImageViewer.Services;

[assembly: Dependency(typeof(ImageViewer.iOS.Services.ImageService))]
namespace ImageViewer.iOS.Services {
    public class ImageService : IImageService {
        public void ShowImageGallery(Action<ImageSource> imageSelectedHandler) {
            UIViewController viewController;
            UIViewController parentViewController;

            viewController = new ImagePickerController(imageSelectedHandler);
            parentViewController = this.FindViewController();
            parentViewController.PresentViewController(viewController, true, null);
        }

        private UIViewController FindViewController() {
            foreach (var window in UIApplication.SharedApplication.Windows) {
                if (window.RootViewController == null) {
                    continue;
                } else {
                    return window.RootViewController;
                }
            }

            return null;
        }
    }
}
```

ImageServiceで使われているImagePickerControllerは以下。

```iOS/Services/ImagePickerController.cs
using System;
using System.IO;
using Xamarin.Forms;
using Foundation;
using UIKit;

namespace ImageViewer.iOS.Services {
    public class ImagePickerController : UIImagePickerController {
        UIImage image;
        Action<ImageSource> mImageSelectedHander;

        public ImagePickerController(Action<ImageSource> imageSelectedHandler) : base() {
            this.mImageSelectedHander = imageSelectedHandler;
        }

        public override void ViewDidLoad() {
            base.ViewDidLoad();
            // set our source to the photo library
            this.SourceType = UIImagePickerControllerSourceType.PhotoLibrary;
            // set what media types
            this.MediaTypes = UIImagePickerController.AvailableMediaTypes(UIImagePickerControllerSourceType.PhotoLibrary);

            this.FinishedPickingMedia += Handle_FinishedPickingMedia;
            this.Canceled += Handle_Canceled;
        }

        // Do something when the
        void Handle_Canceled(object sender, EventArgs e) {
            Console.WriteLine("picker cancelled");
            this.DismissModalViewController(true);
        }

        // This is a sample method that handles the FinishedPickingMediaEvent
        protected void Handle_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e) {
            // determine what was selected, video or image
            bool isImage = false;
            switch (e.Info[UIImagePickerController.MediaType].ToString()) {
            case "public.image":
                Console.WriteLine("Image selected");
                isImage = true;
                break;

            case "public.video":
                Console.WriteLine("Video selected");
                break;
            }

            Console.Write("Reference URL: [" + UIImagePickerController.ReferenceUrl + "]");

            // get common info (shared between images and video)
            NSUrl referenceURL = e.Info[new NSString("UIImagePickerControllerReferenceUrl")] as NSUrl;
            if (referenceURL != null)
                Console.WriteLine(referenceURL.ToString());

            // if it was an image, get the other image info
            if (isImage) {
                // get the original image
                UIImage originalImage = e.Info[UIImagePickerController.OriginalImage] as UIImage;
                if (originalImage != null) {
                    // do something with the image
                    Console.WriteLine("got the original image");
                    image = originalImage;
                }
                // get the edited image
                UIImage editedImage = e.Info[UIImagePickerController.EditedImage] as UIImage;
                if (editedImage != null) {
                    // do something with the image
                    Console.WriteLine("got the edited image");
                    image = editedImage;
                }
                //- get the image metadata
                NSDictionary imageMetadata = e.Info[UIImagePickerController.MediaMetadata] as NSDictionary;
                if (imageMetadata != null) {
                    // do something with the metadata
                    Console.WriteLine("got image metadata");
                }
            }
            // if it's a video
            else {
                // get video url
                NSUrl mediaURL = e.Info[UIImagePickerController.MediaURL] as NSUrl;
                if (mediaURL != null) {
                    //
                    Console.WriteLine(mediaURL.ToString());
                }
            }

            if (image != null) {
                var imageSource = ImageSource.FromStream(() => image.AsPNG().AsStream());
                if (this.mImageSelectedHander != null) {
                    this.mImageSelectedHander(imageSource);
                }
            }

            // dismiss the picker
            this.DismissModalViewController(true);
        }
    }

}
```



## Android

Android側のIImageServiceは以下。

```Dorid/Services/ImageService.cs
using System;
using Xamarin.Forms;
using Android.App;
using Android.Content;
using ImageViewer.Services;

[assembly: Dependency(typeof(ImageViewer.Droid.Services.ImageService))]
namespace ImageViewer.Droid.Services {
    public class ImageService : IImageService {
        public static Action<ImageSource> ImageSelectedHandler { get; private set; }
        public const int RESULT_PICK_IMAGEFILE = 1000;

        public void ShowImageGallery(Action<ImageSource> imageSelectedHandler) {
            ImageService.ImageSelectedHandler = imageSelectedHandler;

            // アクティビティを取得する
            var activity = (Activity)(Forms.Context);

            // ギャラリーを表示する
            var imageIntent = new Intent();
            imageIntent.SetType("image/*");
            imageIntent.SetAction(Intent.ActionGetContent);
            activity.StartActivityForResult(
                Intent.CreateChooser(imageIntent, "Select photo"), RESULT_PICK_IMAGEFILE);
        }
    }
}
```

Androidの場合、アプリの外側にある画像選択Intentを呼び出しているので、選択結果はMainActivityで受け取る必要がある。
MainActivityに以下を追加する。

```Droid/MainActivity.cs
protected override void OnActivityResult(int requestCode, Result resultCode, global::Android.Content.Intent data) {
    try {
        base.OnActivityResult(requestCode, resultCode, data);

        // ImageGalleryからの戻り値
        if (requestCode == ImageService.RESULT_PICK_IMAGEFILE && resultCode == Result.Ok) {
            string filePath = this.GetSelectedItemPath(data);
            ImageSource source = ImageSource.FromFile(filePath);
            if (ImageService.ImageSelectedHandler != null) {
                ImageService.ImageSelectedHandler(source);
            }
        }
    } catch (Exception ex) {
        System.Diagnostics.Debug.WriteLine(ex.Message + System.Environment.NewLine + ex.StackTrace);
    }
}

private string GetSelectedItemPath(Intent data) {
    string filePath = String.Empty;
    // 選択した画像のパスを取得する.
    String strDocId = DocumentsContract.GetDocumentId(data.Data);
    String[] strSplittedDocId = strDocId.Split(':');
    String strId = strSplittedDocId[strSplittedDocId.Length - 1];

    ICursor crsCursor = this.ContentResolver.Query(
        MediaStore.Images.Media.ExternalContentUri
        , new String[] { MediaStore.MediaColumns.Data }
        , "_id=?"
        , new String[] { strId }
        , null);
    if (crsCursor.MoveToFirst()) {
        filePath = crsCursor.GetString(0);
    }
    crsCursor.Close();

    return filePath;
}
```





## サンプル

以上をもとに試しに作成してみたイメージビューワーが以下。

[https://github.com/JunSuzukiJapan/xamarin_forms_image_viewer](https://github.com/JunSuzukiJapan/xamarin_forms_image_viewer)


## 参考

[イメージギャラリーを表示して写真を取得する方法 | Xamarin.Forms](http://itblogdsi.blog.fc2.com/blog-entry-158.html)
[【Unity5】【Android】【Mac】ギャラリーを開いて選択した画像のパスを取得する](http://mslgt.hatenablog.com/entry/2015/05/04/103617)
[How to convert MonoTouch.UIKit.UIImage to Xamarin.Forms.ImageSource?](https://forums.xamarin.com/discussion/17534/how-to-convert-monotouch-uikit-uiimage-to-xamarin-forms-imagesource)
[Choose a Photo From the Gallery](https://developer.xamarin.com/recipes/ios/media/video_and_photos/choose_a_photo_from_the_gallery/)


### 後で読む

本題とは関係ないけど、Androidで"'Forms.Context'is obsolet"と言われてしまう問題関連のリンク。

[Xamarin.Forms: Forms.Context is obsolete](https://stackoverflow.com/questions/47353986/xamarin-forms-forms-context-is-obsolete)
[Xamarin.Forms 2.5 and Local Context on Android](https://www.davidbritch.com/2017/11/xamarinforms-25-and-local-context-on.html)

>解決策としては、Activityがひとつだけのアプリなら、MainActivityにインスタンスへの参照をstaticに持たせてしまうとか…。
>あるいは、MainActivity.GetCurrentActivity()みたいなメソッドを持たせるとか？