Выбор фотографии в библиотеке рисунков
Эта статья описывает создание приложения, позволяющего пользователю выбрать фотографию в библиотеке рисунков на телефоне. Так как Xamarin.Forms не содержит эту функцию, требуется использовать DependencyService
для доступа к собственным API на каждой платформе.
Создание интерфейса
Сначала создайте в общем коде интерфейс для реализации нужной функциональности. В случае с приложением для выбора фотографий требуется всего один метод. Это определяется в интерфейсе IPhotoPickerService
в библиотеке .NET Standard примера кода:
namespace DependencyServiceDemos
{
public interface IPhotoPickerService
{
Task<Stream> GetImageStreamAsync();
}
}
Метод GetImageStreamAsync
определен как асинхронный, так как должен быстро возвращать данные, но он не может возвратить объект Stream
для выбранной фотографии, пока пользователь не просмотрит библиотеку рисунков и не выберет один из них.
Этот интерфейс реализуется на всех платформах с помощью кода, учитывающего особенности конкретной платформы.
Реализация в iOS
Реализация интерфейса IPhotoPickerService
в iOS использует UIImagePickerController
, как описано в разделе Выбор фотографии из коллекции и примере кода.
Реализация для iOS содержится в классе PhotoPickerService
в проекте iOS примера кода. Чтобы сделать этот класс видимым для диспетчераDependencyService
, нужно определить класс с атрибутом [assembly
] типа Dependency
, при этом он должен быть открытым и явно реализовывать интерфейс IPhotoPickerService
:
[assembly: Dependency (typeof (PhotoPickerService))]
namespace DependencyServiceDemos.iOS
{
public class PhotoPickerService : IPhotoPickerService
{
TaskCompletionSource<Stream> taskCompletionSource;
UIImagePickerController imagePicker;
public Task<Stream> GetImageStreamAsync()
{
// Create and define UIImagePickerController
imagePicker = new UIImagePickerController
{
SourceType = UIImagePickerControllerSourceType.PhotoLibrary,
MediaTypes = UIImagePickerController.AvailableMediaTypes(UIImagePickerControllerSourceType.PhotoLibrary)
};
// Set event handlers
imagePicker.FinishedPickingMedia += OnImagePickerFinishedPickingMedia;
imagePicker.Canceled += OnImagePickerCancelled;
// Present UIImagePickerController;
UIWindow window = UIApplication.SharedApplication.KeyWindow;
var viewController = window.RootViewController;
viewController.PresentViewController(imagePicker, true, null);
// Return Task object
taskCompletionSource = new TaskCompletionSource<Stream>();
return taskCompletionSource.Task;
}
...
}
}
Метод GetImageStreamAsync
создает UIImagePickerController
и инициализирует его, чтобы выбрать изображения из библиотеки фотографий. Требуется два обработчика событий: один, когда пользователь выбирает фотографию, а другой, когда пользователь отменяет отображение библиотеки фотографий. Затем метод PresentViewController
отображает библиотеку фотографий для пользователя.
На этом этапе метод GetImageStreamAsync
должен возвращать объект Task<Stream>
вызывающему его коду. Эта задача выполняется только после того, как пользователь закончил взаимодействие с библиотекой фотографий и вызван один из обработчиков событий. Для таких ситуаций крайне важен класс TaskCompletionSource
. Он предоставляет объект Task
соответствующего универсального типа для возврата из метода GetImageStreamAsync
, а позднее этому классу можно подать сигнал о выполнении задачи.
Обработчик событий FinishedPickingMedia
вызывается, когда пользователь выбирает изображение. Однако обработчик предоставляет объект UIImage
и Task
должен возвратить объект .NET Stream
. Это делается в двух шагах: UIImage
объект сначала преобразуется в файл PNG или JPEG в памяти, хранящийся в NSData
объекте, а затем NSData
объект преобразуется в объект .NET Stream
. Вызов метода SetResult
объекта TaskCompletionSource
завершает задачу, предоставляя объект Stream
:
namespace DependencyServiceDemos.iOS
{
public class PhotoPickerService : IPhotoPickerService
{
TaskCompletionSource<Stream> taskCompletionSource;
UIImagePickerController imagePicker;
...
void OnImagePickerFinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs args)
{
UIImage image = args.EditedImage ?? args.OriginalImage;
if (image != null)
{
// Convert UIImage to .NET Stream object
NSData data;
if (args.ReferenceUrl.PathExtension.Equals("PNG") || args.ReferenceUrl.PathExtension.Equals("png"))
{
data = image.AsPNG();
}
else
{
data = image.AsJPEG(1);
}
Stream stream = data.AsStream();
UnregisterEventHandlers();
// Set the Stream as the completion of the Task
taskCompletionSource.SetResult(stream);
}
else
{
UnregisterEventHandlers();
taskCompletionSource.SetResult(null);
}
imagePicker.DismissModalViewController(true);
}
void OnImagePickerCancelled(object sender, EventArgs args)
{
UnregisterEventHandlers();
taskCompletionSource.SetResult(null);
imagePicker.DismissModalViewController(true);
}
void UnregisterEventHandlers()
{
imagePicker.FinishedPickingMedia -= OnImagePickerFinishedPickingMedia;
imagePicker.Canceled -= OnImagePickerCancelled;
}
}
}
Приложению iOS требуется разрешение пользователя для обращения к библиотеке фотографий на телефоне. Добавьте следующий код в раздел dict
файла Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>Picture Picker uses photo library</string>
Реализация в Android
Реализация для Android использует методики, описанные в разделе Выбор изображения и примере кода. Однако метод, вызываемый при выборе пользователем изображения из библиотеки рисунков, представляет собой переопределение OnActivityResult
в классе, производном от Activity
. Поэтому нормальный класс MainActivity
в проекте Android был дополнен полем, свойством и переопределением метода OnActivityResult
:
public class MainActivity : FormsAppCompatActivity
{
internal static MainActivity Instance { get; private set; }
protected override void OnCreate(Bundle savedInstanceState)
{
// ...
Instance = this;
}
// ...
// Field, property, and method for Picture Picker
public static readonly int PickImageId = 1000;
public TaskCompletionSource<Stream> PickImageTaskCompletionSource { set; get; }
protected override void OnActivityResult(int requestCode, Result resultCode, Intent intent)
{
base.OnActivityResult(requestCode, resultCode, intent);
if (requestCode == PickImageId)
{
if ((resultCode == Result.Ok) && (intent != null))
{
Android.Net.Uri uri = intent.Data;
Stream stream = ContentResolver.OpenInputStream(uri);
// Set the Stream as the completion of the Task
PickImageTaskCompletionSource.SetResult(stream);
}
else
{
PickImageTaskCompletionSource.SetResult(null);
}
}
}
}
Переопределение OnActivityResult
указывает выбранный файл изображения с помощью объекта Android Uri
, но его можно преобразовать в объект .NET Stream
, вызвав метод OpenInputStream
объекта ContentResolver
, который был получен из свойства ContentResolver
действия.
Как и реализация для iOS, реализация для Android использует TaskCompletionSource
, чтобы сигнализировать о завершении задачи. Этот объект TaskCompletionSource
определен в виде открытого свойства в классе MainActivity
. Это позволяет ссылаться на свойство в классе PhotoPickerService
проекта Android. Это класс с методом GetImageStreamAsync
:
[assembly: Dependency(typeof(PhotoPickerService))]
namespace DependencyServiceDemos.Droid
{
public class PhotoPickerService : IPhotoPickerService
{
public Task<Stream> GetImageStreamAsync()
{
// Define the Intent for getting images
Intent intent = new Intent();
intent.SetType("image/*");
intent.SetAction(Intent.ActionGetContent);
// Start the picture-picker activity (resumes in MainActivity.cs)
MainActivity.Instance.StartActivityForResult(
Intent.CreateChooser(intent, "Select Picture"),
MainActivity.PickImageId);
// Save the TaskCompletionSource object as a MainActivity property
MainActivity.Instance.PickImageTaskCompletionSource = new TaskCompletionSource<Stream>();
// Return Task object
return MainActivity.Instance.PickImageTaskCompletionSource.Task;
}
}
}
Этот метод обращается к классу MainActivity
по нескольким причинам: за свойством Instance
, за полем PickImageId
, за свойством TaskCompletionSource
и для вызова StartActivityForResult
. Этот метод определен классом FormsAppCompatActivity
, который является базовым классом для MainActivity
.
Реализация на универсальной платформе Windows
В отличие от реализаций для iOS и Android, реализации средства выбора фотографий для универсальной платформы Windows не требуется класс TaskCompletionSource
. Класс PhotoPickerService
использует класс FileOpenPicker
для получения доступа к библиотеке фотографий. Так как метод PickSingleFileAsync
объекта FileOpenPicker
сам является асинхронным, метод GetImageStreamAsync
может просто использовать await
с этим методом (и другими асинхронными методами) и возвращать объект Stream
:
[assembly: Dependency(typeof(PhotoPickerService))]
namespace DependencyServiceDemos.UWP
{
public class PhotoPickerService : IPhotoPickerService
{
public async Task<Stream> GetImageStreamAsync()
{
// Create and initialize the FileOpenPicker
FileOpenPicker openPicker = new FileOpenPicker
{
ViewMode = PickerViewMode.Thumbnail,
SuggestedStartLocation = PickerLocationId.PicturesLibrary,
};
openPicker.FileTypeFilter.Add(".jpg");
openPicker.FileTypeFilter.Add(".jpeg");
openPicker.FileTypeFilter.Add(".png");
// Get a file and return a Stream
StorageFile storageFile = await openPicker.PickSingleFileAsync();
if (storageFile == null)
{
return null;
}
IRandomAccessStreamWithContentType raStream = await storageFile.OpenReadAsync();
return raStream.AsStreamForRead();
}
}
}
Реализация в общем коде
Теперь, когда интерфейс реализован для каждой платформы, общий код в библиотеке .NET Standard может им воспользоваться.
Пользовательский интерфейс содержит элемент Button
, который можно щелкнуть, чтобы выбрать фотографию:
<Button Text="Pick Photo"
Clicked="OnPickPhotoButtonClicked" />
Обработчик событий Clicked
использует класс DependencyService
для вызова GetImageStreamAsync
. После этого выполняется вызов к проекту платформы. Если метод возвращает объект Stream
, обработчик задает свойство Source
объекта image
данным Stream
:
async void OnPickPhotoButtonClicked(object sender, EventArgs e)
{
(sender as Button).IsEnabled = false;
Stream stream = await DependencyService.Get<IPhotoPickerService>().GetImageStreamAsync();
if (stream != null)
{
image.Source = ImageSource.FromStream(() => stream);
}
(sender as Button).IsEnabled = true;
}