Compartir vía


Seleccionar una foto de la biblioteca de imágenes

En este artículo, se explica cómo crear una aplicación que permita al usuario seleccionar una foto de la biblioteca de imágenes del teléfono. Como en Xamarin.Forms no se incluye esta función, es necesario usar DependencyService para acceder a las API nativas en cada plataforma.

Creación de la interfaz

Primero, cree una interfaz en el código compartido que exprese la función deseada. En el caso de una aplicación de selección de fotos, solo se necesita un método. Esto se define en la interfaz IPhotoPickerService de la biblioteca de .NET Standard del código de ejemplo:

namespace DependencyServiceDemos
{
    public interface IPhotoPickerService
    {
        Task<Stream> GetImageStreamAsync();
    }
}

El método GetImageStreamAsync se define como asincrónico porque tiene que devolver resultados rápidamente, pero no puede devolver un objeto Stream para la foto seleccionada hasta que el usuario haya abierto la biblioteca de imágenes y haya seleccionado una.

Esta interfaz se implementa en todas las plataformas que usan código específico de plataforma.

Implementación de iOS

La implementación de iOS de la interfaz IPhotoPickerService usa el objeto UIImagePickerController, como se describe en la receta Seleccionar una foto de la galería y en el código de ejemplo.

La implementación de iOS se contiene en la clase PhotoPickerService del proyecto de iOS del código de ejemplo. Para que el administrador DependencyService pueda ver esta clase, tiene que identificarse con un atributo [assembly] del tipo Dependency y, además, tiene que ser pública e implementar explícitamente la interfaz 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;
        }
        ...
    }
}

El método GetImageStreamAsync crea un elemento UIImagePickerController y lo inicializa para seleccionar imágenes desde la biblioteca de fotos. Se necesitan dos controladores de eventos: uno para cuando el usuario seleccione una foto y otro para cuando cancele la visualización de la biblioteca de fotos. Después, el método PresentViewController muestra la biblioteca de fotos al usuario.

En este momento, el método GetImageStreamAsync tiene que devolver un objeto Task<Stream> al código que realiza la llamada. Esta tarea solo se completa cuando el usuario termina de interactuar con la biblioteca de fotos y se llama a uno de los controladores de eventos. Para situaciones como esta, la clase TaskCompletionSource es esencial. La clase proporciona un objeto Task al tipo genérico adecuado para devolver desde el método GetImageStreamAsync y, después, se puede enviar una señal a la clase cuando se complete la tarea.

Se llama al controlador de eventos FinishedPickingMedia cuando el usuario ha seleccionado una imagen. Pero el controlador proporciona un objeto UIImage y el elemento Task tiene que devolver un objeto Stream de .NET. Esto se realiza en dos pasos: primero, el objeto UIImage se convierte a un archivo PNG o JPEG en memoria almacenado en un objeto NSData y, después, el objeto NSData se convierte a un objeto Stream de .NET. Una llamada al método SetResult del objeto TaskCompletionSource completa la tarea al proporcionar el objeto 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;
        }
    }
}

Una aplicación de iOS necesita el permiso del usuario para acceder a la biblioteca de fotos del teléfono. Agregue el código siguiente a la sección dict del archivo Info.plist:

<key>NSPhotoLibraryUsageDescription</key>
<string>Picture Picker uses photo library</string>

Implementación de Android

La implementación de Android usa la técnica descrita en la receta Seleccionar una imagen y en el código de ejemplo. Pero el método al que se llama cuando el usuario ha seleccionado una imagen desde la biblioteca de imágenes es un reemplazo de OnActivityResult en una clase derivada de Activity. Por este motivo, la clase MainActivity normal del proyecto de Android se ha complementado con un campo, una propiedad y un reemplazo del método 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);
            }
        }
    }
}

El reemplazo OnActivityResult indica el archivo de imagen seleccionado con un objeto Uri de Android, pero esto se puede convertir a un objeto Stream de .NET si se llama al método OpenInputStream del objeto ContentResolver obtenido desde la propiedad ContentResolver de la actividad.

Al igual que la implementación de iOS, en la implementación de Android se usa un elemento TaskCompletionSource para indicar que se ha completado la tarea. Este objeto TaskCompletionSource se define como una propiedad pública de la clase MainActivity. Esto permite hace referencia a la propiedad en la clase PhotoPickerService del proyecto de Android. Esta es la clase con el método 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;
        }
    }
}

Este método accede a la clase MainActivity por varios motivos: para la propiedad Instance, para el campo PickImageId, para la propiedad TaskCompletionSource y para llamar a StartActivityForResult. Este método se define mediante la clase FormsAppCompatActivity, que es la clase base de MainActivity.

Implementación en UWP

Al contrario que en las implementaciones de iOS y Android, la implementación del selector de fotos para la Plataforma universal de Windows no necesita la clase TaskCompletionSource. La clase PhotoPickerService usa la clase FileOpenPicker para acceder a la biblioteca de fotos. Como el método PickSingleFileAsync de FileOpenPicker es en sí asincrónico, el método GetImageStreamAsync puede simplemente usar await con ese método (y otros métodos asincrónicos) y devolver un objeto 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();
        }
    }
}

Implementación en código compartido

Después de implementar la interfaz para cada plataforma, el código compartido de la biblioteca de .NET Standard puede usarla.

La interfaz de usuario incluye un Button en el que se puede hacer clic para elegir una foto:

<Button Text="Pick Photo"
        Clicked="OnPickPhotoButtonClicked" />

El controlador de eventos Clicked usa la clase DependencyService para llamar a GetImageStreamAsync. Como resultado, se produce una llamada al proyecto de la plataforma. Si el método devuelve un objeto Stream, el controlador establece la propiedad Source del objeto image en los datos 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;
}