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


はじめに

Xamarin.FormsはiOSとAndroidの最大公約数的な機能しかないので、画像を選択して表示するだけでも簡単にはいかないようだ。

とりあえずの実現方法を模索してみた。


方針

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


共通部分

DependencyServiceで使うInterfaceを定義する。


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} />みたいに使えるので使いやすい。



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


参考

イメージギャラリーを表示して写真を取得する方法 | Xamarin.Forms

【Unity5】【Android】【Mac】ギャラリーを開いて選択した画像のパスを取得する

How to convert MonoTouch.UIKit.UIImage to Xamarin.Forms.ImageSource?

Choose a Photo From the Gallery


後で読む

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

Xamarin.Forms: Forms.Context is obsolete

Xamarin.Forms 2.5 and Local Context on Android


解決策としては、Activityがひとつだけのアプリなら、MainActivityにインスタンスへの参照をstaticに持たせてしまうとか…。

あるいは、MainActivity.GetCurrentActivity()みたいなメソッドを持たせるとか?