Monday, June 30, 2014

Xamarin.Forms Custom Controls ImageSource

Xamarin.Forms has a great way to handle images cross platform in controls with the ImageSource.  The ImageSource allows you to specify the file name or URI of an image and a Xamarin.Forms handler will figure out how to load them correctly on the appropriate platform.  The following Xamarin site explains how to use the ImageSource for the stock controls that come with Xamarin.Forms

Working with Images

What if you want to make your own custom control, can you write it to use an ImageSource property?  This was the question I faced when creating an ImageButton control for the Xamarin Forms Labs project.  As it turns out it isn't as straightforward as I hoped, but it is possible.

To make this work first I created an ImageButton class that derived from the normal Button as so:

public class ImageButton : Button
{
    public static readonly BindableProperty SourceProperty =
        BindableProperty.Create<ImageButton, ImageSource>(
        p => p.Source, null);
        
    [TypeConverter(typeof(ImageSourceConverter))] 
    public ImageSource Source
    {
        get { return (ImageSource)GetValue(SourceProperty); }
        set { SetValue(SourceProperty, value); }
    }
}

One thing to note is that there is a TypeConverter on the Source property.  That is because the ImageSource cannot be created through a normal constructor passing in the string.  Instead there are factory methods, FromFile and FromUri to create instances of the ImageSource class.  Xamarin.Forms has a ImageSourceConverter class as a TypeConverter for this purpose; unfortunately this class is internal and can't be used directly.  Instead I made my own implementation as below.

public class ImageSourceConverter : TypeConverter
{
    public override bool CanConvertFrom(Type sourceType)
    {
        return sourceType == typeof(string);
    }

    public override object ConvertFrom(CultureInfo culture, object value)
    {
        if (value == null)
        {
            return null;
        }

        var str = value as string;
        if (str != null)
        {
            Uri result;
            if (!Uri.TryCreate(str, UriKind.Absolute, out result) || !(result.Scheme != "file"))
            {
                return ImageSource.FromFile(str);
            }
            return ImageSource.FromUri(result);
        }
        throw new InvalidOperationException(
            string.Format("Conversion failed: \"{0}\" into {1}",
                new[] { value, typeof(ImageSource) }));
    }
}

We need one more thing before we can create our custom renderers.  There are three handlers that convert our ImageSource to the proper platform specific image depending on if a UriImageSource, FileImageSource or StreamImageSource is being used.  Internally Xamarin.Forms uses the Xamarin.Forms.Registrar to resolve out the proper handler.  Unfortunately this class is also internal and can't be used by our custom renderers.  To solve this I created a class and linked it to my platform specific projects where my custom renderers will reside.  This is the class I used to resolve out the proper handler:

#if __Android__
using Xamarin.Forms.Platform.Android;

namespace Xamarin.Forms.Labs.Droid.Controls.ImageButton
#elif __IOS__
using Xamarin.Forms.Platform.iOS;

namespace Xamarin.Forms.Labs.iOS.Controls.ImageButton
#elif WINDOWS_PHONE
using Xamarin.Forms.Platform.WinPhone;

namespace Xamarin.Forms.Labs.WP8.Controls.ImageButton
#endif
{
    public partial class ImageButtonRenderer
    {
        private static IImageSourceHandler GetHandler(ImageSource source)
        {
            IImageSourceHandler returnValue = null;
            if (source is UriImageSource)
            {
                returnValue = new ImageLoaderSourceHandler();
            }
            else if (source is FileImageSource)
            {
                returnValue = new FileImageSourceHandler();
            }
            else if (source is StreamImageSource)
            {
                returnValue = new StreamImagesourceHandler();
            }
            return returnValue;
        }
    }
}

Then I implemented my custom renderers.  I'm not going to show all of their code here and if you want to see the full code check out the Xamarin.Forms.Labs project on Github.  For the iOS platform renderer i resolved out the ImageSource into a iOS UIImage like this:

private async static Task SetImageAsync(ImageSource source, int widthRequest, int heightRequest, UIButton targetButton)
{
    var handler = GetHandler(source);
    using (UIImage image = await handler.LoadImageAsync(source))
    {
        UIGraphics.BeginImageContext(new SizeF(widthRequest, heightRequest));
        image.Draw(new RectangleF(0, 0, widthRequest, heightRequest));
        using (var resultImage = UIGraphics.GetImageFromCurrentImageContext())
        {
            UIGraphics.EndImageContext();
            using (var resizableImage =
                resultImage.CreateResizableImage(new UIEdgeInsets(0, 0, widthRequest, heightRequest)))
            {
                targetButton.SetImage(
                    resizableImage.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal),
                    UIControlState.Normal);
            }
        }
    }
}

On the Android platform's custom renderer for the ImageButton I resolved it out like this:

private async Task<Bitmap> GetBitmapAsync(ImageSource source)
{
    var handler = GetHandler(source);
    var returnValue = (Bitmap)null;

    returnValue = await handler.LoadImageAsync(source, this.Context);

    return returnValue;
}

On the Windows Phone platform I resolved out the ImageSource like this:

private async static Task<System.Windows.Controls.Image> GetImageAsync(ImageSource source, int height, int width)
{
    var image = new System.Windows.Controls.Image();
    var handler = GetHandler(source);
    var imageSource = await handler.LoadImageAsync(source);

    image.Source = imageSource;
    image.Height = Convert.ToDouble(height / 2);
    image.Width = Convert.ToDouble(width / 2);
    return image;
}

That's how I implemented the Source property on my custom ImageButton control and now I get all the cross platform image handling goodness.  I hope this helps if you need to make a Xamarin.Forms custom control that needs to implement a cross platform image.

No comments:

Post a Comment