Edit one:
Changes made based on Joseph's answer:
In bytesToDrawable(byte[] imageBytes):
Changed the following : Using BitmapDrawable(Resources res, Bitmap bitmap) instead of BitmapDrawable(Bitmap bitmap):
return new BitmapDrawable(ApplicationConstants.ref_currentActivity.getResources(),BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, options));
This is the result of that change: Slightly Different Problem:
Question:
If I'm using the new constructor for bitmap drawable and it scales images for the required target density, do I need to use my calculateSampleSize method still?
Original Question:
Hi friends,
My application is module based and thus images specific to that module are only loaded from the jar(module) that contains them, and not from the main application.
Each module has its own ModularImageLoader - which basically allows me to fetch Drawables based on the name of the image found in the jar.
The constructor takes in the zipFile(Module A) and a list of filesnames(any file ending with ".png" from the zip).
Research Conducted:
I have used the following: Link to Developer Page on Loading bitmaps efficiently
Initially I was creating images sized for each density, but now I just have one set of image icons sized 96x96.
If the screen density is less than xhdpi, I load smaller sampled sizes of the 96x96 image - as 36x36(for ldpi), 48x48(for mdpi), 72x72(for hdpi). Otherwise I just return the 96x96 image. (Look at method calculateSampleSize() and bytesToDrawable())
I think its easier to understand the concept with the code: So here's ModularImageLoader
Code:
public class ModularImageLoader
{
public static HashMap<String, Drawable> moduleImages = new HashMap<String, Drawable>();
public static int reqHeight = 0;
public static int reqWidth = 0;
public ModularImageLoader(ZipFile zip, ArrayList<String> fileNames)
{
float sdpi = ApplicationConstants.ref_currentActivity.getResources().getDisplayMetrics().density;
if(sdpi == 0.75)
{
reqHeight = 36;
reqWidth = 36;
}
else if(sdpi == 1.0)
{
reqHeight = 48;
reqWidth = 48;
}
else if (sdpi == 1.5)
{
reqHeight = 72;
reqWidth = 72;
}
else if (sdpi == 2.0)
{
reqHeight = 96;
reqWidth = 96;
}
String names = "";
for(String fileName : fileNames)
{
names += fileName + " ";
}
createByteArrayImages(zip, fileNames);
}
public static Drawable findImageByName(String imageName)
{
Drawable drawableToReturn = null;
for (Entry<String, Drawable> ent : moduleImages.entrySet())
{
if(ent.getKey().equals(imageName))
{
drawableToReturn = ent.getValue();
}
}
return drawableToReturn;
}
private static void createByteArrayImages(ZipFile zip, ArrayList<String> fileNames)
{
InputStream in = null;
byte [] temp = null;
int nativeEndBufSize = 0;
for(String fileName : fileNames)
{
try
{
in = zip.getInputStream(zip.getEntry(fileName));
nativeEndBufSize = in.available();
temp = toByteArray(in,nativeEndBufSize);
// get rid of .png
fileName = fileName.replace(".png", "");
fileName = fileName.replace("Module Images/", "");
moduleImages.put(fileName, bytesToDrawable(temp));
}
catch(Exception e)
{
System.out.println("getImageBytes() threw an exception: " + e.toString());
e.printStackTrace();
}
}
try
{
in.close();
}
catch (IOException e)
{
System.out.println("Unable to close inputStream!");
e.toString();
e.printStackTrace();
}
}
public static byte[] toByteArray(InputStream is, int length) throws IOException
{
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int l;
byte[] data = new byte[length];
while ((l = is.read(data, 0, data.length)) != -1)
{
buffer.write(data, 0, l);
}
buffer.flush();
return buffer.toByteArray();
}
public static Drawable bytesToDrawable(byte[] imageBytes)
{
try
{
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, options);
String imageType = options.outMimeType;
Log.d("ImageInfo : ", "Height:" + imageHeight +",Width:" +imageWidth + ",Type:" + imageType);
options.inJustDecodeBounds = false;
//Calculate sample size
options.inSampleSize = calculateSampleSize(options);
return new BitmapDrawable(BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, options));
}
catch(Exception e)
{
Message.errorMessage("Module Loading Error", "The images in this module are too large to load onto cell memory. Please contact your administrator",
"Source of error: ModularImageLoader - bytesToDrawable method", e.toString());
return null;
}
}
public static int calculateSampleSize(BitmapFactory.Options options)
{
// raw height and width of the image itself
int sampleSize = 1;
int height = options.outHeight;
int width = options.outWidth;
if(height > reqHeight || width > reqWidth)
{
if(width > height)
{
sampleSize = Math.round((float)height / (float)reqHeight);
}
else
{
sampleSize = Math.round((float)width / (float)reqWidth);
}
}
return sampleSize;
}
}
Problem:
The image below shows 4 running emulators, these are their specifications and how I set them in the eclipse AVD:
LDPI: density 120, Skin QVGA MDPI: density 160, Skin HVGA HDPI: density 240, Skin WVGA800 XHDPI:density 320, Skin 800x1280
Image Showing Problem:
Question:
Based on the code - in the XHDPI window, why is the contacts image so tiny? The News image is also 96x96 (Except its loaded from the main application - so its under res>XHDPI). The thing is, I see it loading fine for MDPI screens and HDPI screens, but its weird for the rest. Any ideas?
ldpi - 36x36 mdpi - 48x48 hdpi - 72x72 xhdpi - 96x96
It would be great if these were all pure multiples of each other as the bitmap factory handles sample size in integers and therefore the sample size needs to be a whole number (no trailing decimals to be fully accurate).
The solution:
Before I started sampling, I had 1 image for "every" screen type, and if that image had a pressed state, I'd have 2 separate images for that.
Therefore for one image - I actually needed 4 in the application, and if that image had pressed states - one image would need 8 images.
My main aim was to cut down on the number of images, so that I don't overload the bitmap heap allocation and possibly throw a out of memory exception, I've seen this exception thrown for me, when my bitmap size is perfectly reasonable (I believe its something to do with the number of images on the heap as well as their respective images sizes (correct me if I'm wrong please..)) and I wanted to of course, cut down on my module size.
So I decided on this: Have 2 images for each image - one in size 72 and one in size 96 - this way I'd have the icons I needed for screens with xhdpi and hdpi density, and I could sample(simply) down to ldpi and mdpi when required.
72/2 = 36 96/2 = 48
This way I'd only have 2 images for every image and worst case, if that image had pressed states, I'd have 4 images. That's cutting down the image size bank by almost 50% and making my modules a lot smaller. I noticed a change in module size from 525 kb to about 329.
This really was what I was aiming for.
Thanks everyone for all the help! If anyone has any questions, please feel free to leave comments and I will get back to you as soon as I can.