Xamarin.Forms and large images in android apps

Are you getting out of memory exception when your running your Android app? It's common that the reason is that a large image is loaded. If it's the case, take a look at this article at the Xamarin developer portal, http://developer.android.com/training/displaying-bitmaps/load-bitmap.html.

But how to implement it when you're building your apps with Xamarin.Forms? In this post I will show one solution how to implement it. We will do it with an custom view and a custom renderer.

First we will create the new view that will inherit from the standard Image view, I will name it LargeImage. While we doesn't want the default behavior we need to create our own source property of type string, I name it ImageSource. While we just want to change the behavior for the Android app and not for Windows and iOS we will set the base Source property in the property changed handler of the ImageSource property if the code not is running on Android.

public class LargeImage : Image
{
        public static readonly BindableProperty ImageSourceProperty =
        BindableProperty.Create("ImageSource", typeof(string), typeof(LargeImage), default(string), propertyChanged: (bindable, oldValue, newValue) => 
        {
            if (Device.OS != TargetPlatform.Android)
            {
                var image = (LargeImage)bindable;

                var baseImage = (Image)bindable;
                baseImage.Source = image.ImageSource; 
            }
        });

        public string ImageSource
        {
            get { return GetValue(ImageSourceProperty) as string; }
            set { SetValue(ImageSourceProperty, value); }
        }
}

Next step is to create a renderer for our new view. While we need the default Image behavior except for handling the source the renderer will inherit from ImageRenderer.

This renderer will only work for images in the Drawable folder, so if you have other type of image sources you need to modify the code.

In the renderer we need to handle the ImageSource property, we will do that in the OnPropertyChanged method. While we doesn't want to run the code before the image has width and height we added a if-statement that check if width and height is greater than zero. But we just want it to run once because of that width and height is greater than zero, because of that i have added a flag that I named _isDecoded. If ImageSource changed the code will run because that e.PropertyName will be ImageSource.

[assembly: ExportRenderer(typeof(LargeImage), typeof(LargeImageRenderer))]
namespace SampleApp.Droid.Renderers
{
    public class LargeImageRenderer : ImageRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs e)
        {
            base.OnElementChanged(e);
        }

        private bool _isDecoded;
        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            var largeImage = (LargeImage)Element;

            if ((Element.Width > 0 && Element.Height > 0 && !_isDecoded) || (e.PropertyName == "ImageSource" && largeImage.ImageSource != null)) 
            {
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.InJustDecodeBounds = true;

                //Get the resource id for the image
                var field = typeof(Resource.Drawable).GetField(largeImage.ImageSource.Split('.').First());
                var value = (int)field.GetRawConstantValue();

                BitmapFactory.DecodeResource(Context.Resources, value,options);

                //The with and height of the elements (LargeImage) will be used to decode the image
                var width = (int)Element.Width;
                var height = (int)Element.Height;
                options.InSampleSize = CalculateInSampleSize(options, width, height);

                options.InJustDecodeBounds = false;
                var bitmap = BitmapFactory.DecodeResource(Context.Resources, value, options);

                //Set the bitmap to the native control
                Control.SetImageBitmap(bitmap);

                _isDecoded = true;
            }

        }
        public int CalculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)
        {
            // Raw height and width of image
            float height = options.OutHeight;
            float width = options.OutWidth;
            double inSampleSize = 1D;

            if (height > reqHeight || width > reqWidth)
            {
                int halfHeight = (int)(height / 2);
                int halfWidth = (int)(width / 2);

                // Calculate a inSampleSize that is a power of 2 - the decoder will use a value that is a power of two anyway.
                while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth)
                {
                    inSampleSize *= 2;
                }
            }

            return (int)inSampleSize;
        }
    }
}


  12/2/2015 - 8:28 AM