July 23, 2015

Avoiding ImageBitmap OutOfMemoryException and Rounded corner Image in android xamarin

Brief:
Here i will write about how to create rounded corner/Circular Image from the image bitmap and also how to handle large images in xamarin.android.


In  previous post i explained,How to use Google Place API with Autocomplete in Xamarin Android,Integration of google map v2 in xamaron androidWorking on the wrong position and item click event issues with ListView in android xamarin.

Description : 
Circular shaped image display is the common requirement in most of the android application.
Let us build one sample application to display image. Here image need to be fetch from SD card/Phone memory.

Whenever we need to load image bitmap to imageview it is must and should to analyze the image dimension and size.
Image captured from the device camera usually takes high resolution then the screen density and size .
RAM space allocated to the app may not enough to handle it properly which leads to OutOfMemoryException. 
Loading smaller subsampled version of image is the best approach to solve OutOfMemory issue.This can be achieved by reducing Image resolution,

as per the xamarin document Reducing the image resolution doesn't change any visual effect of the image.

Here i will take High resolution image 3120*4208 with size 5.08MB which is to be displayed with rounded corner from SD card.
Step 1: Get BitmapOptions to find out the image current dimension.
string strImgPath =System.IO.Path.Combine( Android.OS.Environment.ExternalStorageDirectory.AbsolutePath,"Sample","sample.jpg") ;

BitmapFactory.Options option = await ImageHelper.GetBitmapOptionsOfImage ( strImgPath );

[Find called function below]

Step 2: For the target height,width of the UI component calculate InSampleSize ratio. InSampleSize value is used to scale down the
image and load smaller version of the image. InSampleSize value varies according to the image resolution and it is Factor of 2.

//avoid single quote '<'
var imgProfilePic = FindViewById'<'imageview'>' ( Resource.Id.myImg );

 int width=imgProfilePic.MeasuredWidth/2;  [Note : MeasuredWidth and height will work only after image drawn fully]
 int height=imgProfilePic.MeasuredHeight/2; 

 var bitmapSampled = await LoadScaledDownBitmapForDisplayAsync ( strImgPath , option , width , height ); 

Still if it had OutOfMemory issue,consider adding below line in manifest file
android:largeHeap="true"

 Step 3: Get rounded corner image bitmap from the optimized bitmap obtained from the step2.  
Here the important note is, to get rounded corner Image the height and width of the input image bitmap should have equal bitmap height and width.

  int intRoundPicResolution; 
   //only to get circle shape 
  intRoundPicResolution= Math.Min(bitmapSampled.Width,bitmapSampled.Height);

  bitmapSampled = Bitmap.CreateScaledBitmap ( bitmapSampled , intRoundPicResolution , intRoundPicResolution , false );
  int imgRadius = ( bitmapSampled.Width ) / 2; 

  using ( var bitmapRoundedCorner = GetRoundedCornerBitmap ( bitmapSampled , imgRadius ) )
  {
   RecycleBitmap (imgProfilePic);
   imgProfilePic.SetImageBitmap ( bitmapRoundedCorner ); 
  } 

Step 4: Write back the sampled down Image bitmap to the phone storage[This requires WRITE_EXTERNAL_STORAGE]. 
Now we can see the optimized image resolution and size. In my phone storage it is showing 390*526 and Size : 497.94KB



Conclusion : 
This is the demonstration  on image sub sampling of a large image while keeping the same visual effect and also converting image bitmap into circular shape.
Look at the sample code at: https://github.com/suchithm/SampleImageBitMap

[ImageHelper.cs]
// If you would like to create a circle of the image set pixels to half the width of the image.
  internal static   Bitmap GetRoundedCornerBitmap(Bitmap bitmap, int pixels)
  {
   Bitmap output = null;

   try
   {
    output = Bitmap.CreateBitmap(bitmap.Width, bitmap.Height, Bitmap.Config.Argb8888);
    Canvas canvas = new Canvas(output);

    Color color = new Color(66, 66, 66);
    Paint paint = new Paint();
    Rect rect = new Rect(0, 0, bitmap.Width, bitmap.Height);
    RectF rectF = new RectF(rect);
    float roundPx = pixels;

    paint.AntiAlias = true;
    canvas.DrawARGB(0, 0, 0, 0);
    paint.Color = color;
    canvas.DrawRoundRect(rectF, roundPx, roundPx, paint);

    paint.SetXfermode(new PorterDuffXfermode(PorterDuff.Mode.SrcIn));
    canvas.DrawBitmap(bitmap, rect, rect, paint);
   }
   catch (System.Exception err)
   {
    System.Console.WriteLine ("GetRoundedCornerBitmap Error - " + err.Message);
   }

   return output;
  }
  //to get bitmap option current image with it's height and width 
  internal async static Task'<'BitmapFactory.Options'>' GetBitmapOptionsOfImage(string strFileName)
  {
   BitmapFactory.Options options = new BitmapFactory.Options
   {
    InJustDecodeBounds = true //avoids memory allocation during decoding
   }; 
   // The result will be null because InJustDecodeBounds == true.
    await BitmapFactory.DecodeFileAsync(strFileName, options);

   int imageHeight = options.OutHeight;
   int imageWidth = options.OutWidth;
   Console.WriteLine ( "height : " + imageHeight + " width : " + imageWidth );
//   _originalDimensions.Text = string.Format("Original Size= {0}x{1}", imageWidth, imageHeight); 
   return options;
  }
  internal async static Task'<'Bitmap'>' LoadScaledDownBitmapForDisplayAsync(string strFileName, BitmapFactory.Options options, int reqWidth, int reqHeight)
  {
   // Calculate inSampleSize
   options.InSampleSize = CalculateInSampleSize(options, reqWidth, reqHeight);
   // Decode bitmap with inSampleSize set
   options.InJustDecodeBounds = false; //to let memory allocation during decoding 
   return await BitmapFactory.DecodeFileAsync(strFileName, options);
  }
  //Calculates InSampleSize if required
  internal static 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;
  } 

  // This method will recyle the memory help by a bitmap in an ImageView
  internal static void RecycleBitmap(this ImageView imageView)
  {
   if (imageView == null) {
    return;
   } 
   Drawable toRecycle = imageView.Drawable;
   if (toRecycle != null) {
    ((BitmapDrawable)toRecycle).Bitmap.Recycle ();
   }
  }