There's been countless times that I've encountered memory leaks on android. 9 out of 10 times, it was because I had high resolution images in my resource folder. For example, if you have an image of 3000 x 3000, and its display resolution on screen is only 300 x 300, you would only want that 300 x 300 bitmap in the memory. Sadly, the android implementation of the image class in Xamarin.forms doesn't do that for you. So I came up with a custom image renderer that does.
Big Image class
using Xamarin.Forms;
namespace YOURPROJECTNAME
{
public class BigImage : Image
{
}
}
Custom Big Image Renderer:
using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Renderscripts;
using Android.Widget;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using YOURPROJECTNAME;
using YOURPROJECTNAME.Droid;
[assembly: ExportRenderer(typeof(BigImage), typeof(BigImageRenderer))]
namespace BlurredImageTest.Droid
{
public class BigImageRenderer : ViewRenderer<BigImage, ImageView>
{
private bool _isDisposed;
public BigImageRenderer()
{
AutoPackage = false;
}
protected override void OnElementChanged(ElementChangedEventArgs<BigImage> e)
{
base.OnElementChanged(e);
if (Control == null)
{
var imageView = new BigImageView(Context);
SetNativeControl(imageView);
}
UpdateBitmap(e.OldElement);
UpdateAspect();
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == Image.SourceProperty.PropertyName)
{
UpdateBitmap(null);
return;
}
if (e.PropertyName == Image.AspectProperty.PropertyName)
{
UpdateAspect();
}
}
protected override void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
BitmapDrawable bitmapDrawable;
if (disposing && Control != null && (bitmapDrawable = (Control.Drawable as BitmapDrawable)) != null)
{
Bitmap bitmap = bitmapDrawable.Bitmap;
if (bitmap != null)
{
bitmap.Recycle();
bitmap.Dispose();
}
}
base.Dispose(disposing);
}
private void UpdateAspect()
{
using (ImageView.ScaleType scaleType = ToScaleType(Element.Aspect))
{
Control.SetScaleType(scaleType);
}
}
private static ImageView.ScaleType ToScaleType(Aspect aspect)
{
switch (aspect)
{
case Aspect.AspectFill:
return ImageView.ScaleType.CenterCrop;
case Aspect.Fill:
return ImageView.ScaleType.FitXy;
}
return ImageView.ScaleType.FitCenter;
}
private async void UpdateBitmap(Image previous = null)
{
Bitmap bitmap = null;
ImageSource source = Element.Source;
if (previous == null || !object.Equals(previous.Source, Element.Source))
{
SetIsLoading(true);
((BigImageView)base.Control).SkipInvalidate();
Control.SetImageResource(global::Android.Resource.Color.Transparent);
if (source != null)
{
try
{
bitmap = await GetImageFromImageSource(source, Context);
}
catch (TaskCanceledException)
{
}
catch (IOException)
{
}
catch (NotImplementedException)
{
}
}
if (Element != null && object.Equals(Element.Source, source))
{
if (!_isDisposed)
{
Control.SetImageBitmap(bitmap);
if (bitmap != null)
{
bitmap.Dispose();
}
SetIsLoading(false);
((IVisualElementController)base.Element).NativeSizeChanged();
}
}
}
}
private async Task<Bitmap> GetImageFromImageSource(ImageSource imageSource, Context context)
{
IImageSourceHandler handler;
if (imageSource is FileImageSource)
{
int width = -1, height = -1;
if ((int)((Image)Element).Width != -1)
width = (int)((Image)Element).Width;
else if ((int)((Image)Element).WidthRequest != -1)
width = (int)((Image)Element).WidthRequest;
if ((int)((Image)Element).Height != -1)
height = (int)((Image)Element).Height;
else if ((int)((Image)Element).HeightRequest != -1)
height = (int)((Image)Element).HeightRequest;
handler = new CustomFileImageSourceHandler { reqWidth = width, reqHeight = height };
}
else if (imageSource is StreamImageSource)
{
handler = new StreamImagesourceHandler(); // sic
}
else if (imageSource is UriImageSource)
{
handler = new ImageLoaderSourceHandler(); // sic
}
else
{
throw new NotImplementedException();
}
var originalBitmap = await handler.LoadImageAsync(imageSource, context);
return originalBitmap;
}
private class BigImageView : ImageView
{
private bool _skipInvalidate;
public BigImageView(Context context) : base(context)
{
}
public override void Invalidate ()
{
if (this._skipInvalidate) {
this._skipInvalidate = false;
return;
}
base.Invalidate ();
}
public void SkipInvalidate ()
{
this._skipInvalidate = true;
}
}
private static FieldInfo _isLoadingPropertyKeyFieldInfo;
private static FieldInfo IsLoadingPropertyKeyFieldInfo
{
get
{
if (_isLoadingPropertyKeyFieldInfo == null)
{
_isLoadingPropertyKeyFieldInfo = typeof(Image).GetField("IsLoadingPropertyKey", BindingFlags.Static | BindingFlags.NonPublic);
}
return _isLoadingPropertyKeyFieldInfo;
}
}
private void SetIsLoading(bool value)
{
var fieldInfo = IsLoadingPropertyKeyFieldInfo;
((IElementController)base.Element).SetValueFromRenderer((BindablePropertyKey)fieldInfo.GetValue(null), value);
}
}
}
Custom file Image loader:
using System.Threading;
using System.Threading.Tasks;
using Xamarin.Forms;
using Android.Graphics;
using Android.Content;
using Xamarin.Forms.Platform.Android;
namespace YOURPROJECTNAME.Droid
{
public class CustomFileImageSourceHandler : IImageSourceHandler
{
public int reqWidth;
public int reqHeight;
public async Task<Bitmap> LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default(CancellationToken))
{
var imageLoader = imagesource as FileImageSource;
if (imageLoader != null && imageLoader.File != null)
{
if (reqWidth != -1 && reqHeight != -1)
{
int resID = GetImageResourceID(imageLoader.File);
BitmapFactory.Options options = await GetBitmapOptionsOfImage(resID);
return await LoadScaledDownBitmapForDisplayAsync(resID, options, reqWidth, reqHeight);
}
else
{
var handler = new FileImageSourceHandler();
return await handler.LoadImageAsync(imagesource, context, cancelationToken);
}
}
return null;
}
int GetImageResourceID(string fileName)
{
fileName = fileName.Replace(".png", "").Replace(".jpg", "");
var resField = typeof(Resource.Drawable).GetField(fileName);
var resID = (int)resField.GetValue(null);
return resID;
}
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;
}
async Task<BitmapFactory.Options> GetBitmapOptionsOfImage(int resID)
{
BitmapFactory.Options options = new BitmapFactory.Options
{
InJustDecodeBounds = true
};
// The result will be null because InJustDecodeBounds == true.
Bitmap result = await BitmapFactory.DecodeResourceAsync(Forms.Context.Resources, resID, options);
int imageHeight = options.OutHeight;
int imageWidth = options.OutWidth;
//_originalDimensions.Text = string.Format("Original Size= {0}x{1}", imageWidth, imageHeight);
return options;
}
async Task<Bitmap> LoadScaledDownBitmapForDisplayAsync(int resID, BitmapFactory.Options options, int reqWidth, int reqHeight)
{
// Calculate inSampleSize
options.InSampleSize = CalculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.InJustDecodeBounds = false;
return await BitmapFactory.DecodeResourceAsync(Forms.Context.Resources, resID, options);
}
}
}
My solution is inspired by the following posts:
http://blog.adamkemp.com/2015/05/blurred-image-renderer-for-xamarinforms.html
https://developer.xamarin.com/recipes/android/resources/general/load_large_bitmaps_efficiently/