How to optimize a custom ImageView to be used in a GridView? - android

I have developed this custom ImageView class to override some of the default behavior to fit my needs. Let me describe what this custom ImageView does...
Let's say you have a bunch of icons to display in GridView both in the drawable-mdpi and drawable-hdpi folder, they are 48x48px and 72x72px in size, respectively. There are no icons available in the drawable-xhdpi folder. The GridView attributes are so that all the icons size will be in 48x48dp (this will translate to 48px, 72px and 96px for mpdi, hdpi and xhdpi densities, respectively).
Since there are no icons in the drawable-xhdpi folder, when this app is ran on a device with such density, the icons will be pulled from the drawable-hdpi folder. And since they are only 72px and the xhdpi devices are expecting 96px images, the icons will be stretched to fill the remaining pixels.
This is the behavior my custom ImageView attempts to override. With my custom component, what will happen is that the images will simply not get stretched. For instance, in the example above using my class, each ImageView inside the GridView will still be 96x96px (because of the 48x48dp size defined) but the images used are from the drawable-hdpi folder which are 72x72px. What will happen is that these images from the drawable-hdpi folder will be placed in the center of the ImageView which is 96x96px in size without stretching the image to fit the whole view size.
If the above is confusing, let's try with a few pictures. The example below does not use GridView, I'm trying to simplify the idea behind my custom class. These are the source pictures I'm using for this example:
This is the result on HDPI device:
And this is the result on XHDPI device:
The code for the layout on the screenshots above is this:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Standard ImageView:"
android:textAppearance="?android:attr/textAppearanceLarge"/>
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="10dp"
android:scaleType="center"
android:background="#FFEEEE"
android:src="#drawable/ic_female"/>
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="10dp"
android:scaleType="center"
android:background="#FFEEEE"
android:src="#drawable/ic_male"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Custom UnscaledImageView:"
android:textAppearance="?android:attr/textAppearanceLarge"/>
<com.sampleapp.widget.UnscaledImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="10dp"
android:scaleType="center"
android:background="#FFEEEE"
android:src="#drawable/ic_female"/>
<com.sampleapp.widget.UnscaledImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="10dp"
android:scaleType="center"
android:background="#FFEEEE"
android:src="#drawable/ic_male"/>
</LinearLayout>
Is it more clear now? This is what I want to do and this is working nicely, besides a small performance issue... Now let me post the code I'm using for such custom component:
attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="UnscaledImageView">
<attr name="android:src" />
</declare-styleable>
</resources>
UnscaledImageView.java:
public class UnscaledImageView extends ImageView {
private int mDeviceDensityDpi;
public UnscaledImageView(Context context) {
super(context);
mDeviceDensityDpi = getResources().getDisplayMetrics().densityDpi;
}
public UnscaledImageView(Context context, AttributeSet attrs) {
super(context, attrs);
mDeviceDensityDpi = getResources().getDisplayMetrics().densityDpi;
TypedArray styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.UnscaledImageView);
int resourceId = styledAttrs.getResourceId(R.styleable.UnscaledImageView_android_src, 0);
if(resourceId != 0) {
setUnscaledImageResource(resourceId);
}
styledAttrs.recycle();
}
public void setUnscaledImageResource(int resId) {
setImageBitmap(decodeBitmapResource(resId));
}
#SuppressWarnings("deprecation")
public void setUnscaledBackgroundResource(int resId) {
BitmapDrawable drawable = new BitmapDrawable(null, decodeBitmapResource(resId));
drawable.setTargetDensity(mDeviceDensityDpi);
drawable.setGravity(Gravity.CENTER);
setBackgroundDrawable(drawable);
}
private Bitmap decodeBitmapResource(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDensity = mDeviceDensityDpi;
return BitmapFactory.decodeResource(getResources(), resId, options);
}
}
So, this class will do it's thing if the UnscaledImageView view is used in XML layouts or directly initialized in code. I've also provided 2 methods so the image can be changed in code while keeping it from being stretched. As you can see, these methods only take resource ids, so far I haven't felt the need to use drawables or bitmaps directly.
Now the real issue I'm having with this...
If this class is used as single image view in some layout, no problem, it's only decoding one image. But if it's used in a GridView where there can be like 40 icons (I'm taking this value from what really happens on my app running on my xhdpi device) visible at the same time, scrolling the GridView will be very slow because the decodeBitmapResource() is calling BitmapFactory.decodeResource() for each and every image.
This is my problem and that is my question. How can I optimize this? If possible, at all...

Putting those images into the res/drawable-nodpi/ could do what you want (I'm saying to put different resolution images side by side).
It would be a bit tricky because probably you'd have to follow a naming convention to be able to find the best resource for a given image that you are trying to draw. Probably this will require you to try finding images by their name and that's not a very efficient way to retrieve resources.
The way I imagin this is: on the layout (or anywhere else), you specify the name (string, not id!) of the image resource you want to use.
In that nodpi folder, you'd have the images with a suffix for the intended screen density.
Then, in the setter method you have to try different combinations in order to find the best available resource.
Problem for you to think: what if you're scaling down an image? The resource would be bigger than the view where you'd draw it!

Although the answer by Pedro Loureiro could be a possible solution, I decided to take a different approach after realizing something...
I've timed both the native ImageView loading and my UnscaledImageView loading in a GridView, non-scientifically of course and I've realized that my class loads all the images a little bit faster than the native one. Maybe the native methods have something else going on, besides decoding the resource (they still have to do it, right?) while my class simply decodes a resource (using BitmapFactory) and that's basically it.
I thought that it was my class that was making the GridView kinda slow while scrolling but after a few more tests, using the original ImageView without any tweaks, also revealed to be a little choppy while scrolling the GrivView.
The solution I found to solve this issue (either with my class or the native one) was to cache the icons in the GridView and for that I used the LruCache which is the recommended way of caching images in a GridView.
So that's the solution I'll be using to solve my issue. For more details please refer to the official training guide: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
For reference, I've also found the following tutorial useful: http://andrewbrobinson.com/2012/03/05/image-caching-in-android/

Related

Set higher Drawable resolution for big images

I have in my project a list of PNG icons in many scales (mdpi at 32px, hdpi at 48px, xhdpi at 64px and xxhdpi at 96px, automatically created by Android Studio), and I get one Activity where I want to display this icons in two distinct sizes, some bigger than the others.
Problem is that the bigger ones (the only ones where I really define layout widths and heights) use low res versions of the drawable instead of nicer ones.
Here is my XML code:
<!-- small icon -->
<ImageView
android:layout_weight="1"
android:layout_width="1dp"
android:layout_height="wrap_content"
android:src="#drawable/ic_icon_like" />
<!-- ... -->
<!-- big icon -->
<ImageView
android:layout_weight="1"
android:src="#drawable/ic_icon_like"
android:layout_width="90dp"
android:layout_height="90dp" />
And here is the result:
As for a comparison, here's the xxhdpi version of PNG of the Drawable, clearly better than the big one I got.
I'm new to Android, so I suppose I'm doing something wrong, but I cannot figure what. Should I define higher res pictures per resolution? Should I provide an ic_icon_like and an ic_icon_like_big for the two displayed versions?
Well, I finally moved on and used a webfont instead, as I get all my icons in SVG formats.
Sizing and coloring are way simpler like this.
So I added an /app/assets/fonts/webfont.ttf file in my project, created an IconView heriting from TextView like this:
public class IconView extends TextView {
public IconView(Context context, AttributeSet attrs) {
super(context, attrs);
// here I trully use a cache, but remove it for a full example
this.setTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/webfont.ttf"));
// the following allows me to use a default color if no 'textColor' attribute is provided
int[] set = {android.R.attr.textColor};
TypedArray attributes = context.obtainStyledAttributes(attrs, set);
ColorDrawable color = (ColorDrawable) attributes.getDrawable(0);
attributes.recycle();
if (color == null) {
this.setTextColor(MY_DEFAULT_COLOR);
}
}
}
Also I added constants in strings.xml for each icon in my webfont, as:
<string name="icon_like"></string>
And used the whole in my XML activities and fragments like this:
<com.mySociety.myProject.IconView android:text="#string/icon_like" />

Efficient Bitmaps

Recently, while trying to load a image in imageView I ran into "Out of Heap Memory" error. So I looked on internet and found the way to optimise the images.
I am looking forward to finding the best way to optimise a bitmap.
Currently, I have an ImageView which has 200dp as hight and "match_parent" for width. Basically it fills the screen horizontally and takes 200dp vertically.
I am trying to achieve something like in this image.
Here is my master plan to do so.
1) Create a layout for 1 row (ImageView, TextView, Black Translucent bar behind text)
2) Use RecyclerView and inflate the data
Currently I am stuck on Step 1 (not satisfied with code).
What I did is create an XML layout for the row.
<RelativeLayout
android:id="#+id/layout_adventure_fragment"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="200dp">
<ImageView
android:id="#+id/imageView_adventure_home_screen_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:background="#drawable/textview_background_gradient"
android:layout_alignParentBottom="true"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:paddingLeft="15dp"
android:textColor="#FFFFFF"
android:textSize="25sp"
android:id="#+id/textView_adventure_name_home_screen_fragment"
android:layout_width="match_parent"
android:layout_height="55dp"
android:text="Demo Text"/>
</RelativeLayout>
I'll adjust TextView as per Typography guidelines later.
Ok, so basically ImageView is 200dp tall.
To compress my images I used the guide at Android training page which uses inSampleSize and loads image in an AsyncTask.
To calculate the required height I used something I am not sure if it is correct way or not. Since I care to compress image only for height I passed only height to calculate inSampleSize.
DisplayMetrics disp = getApplicationContext().getResources().getDisplayMetrics();
int density = disp.densityDpi;
width = disp.widthPixels;
height = (int)((float)(200.0/160.0) * (float)density);
Bitmap b =decodeSampledBitmapFromResource(getResources(),R.drawable.shake,height);
loadBitmap(R.drawable.shake,iV);
Finally I extracted thumbnail from the final bitmap for the required width and height to show user an image which fills the imageView instead of being just in the centre with margins.
imageView.setImageBitmap(ThumbnailUtils.extractThumbnail(bitmap,width,height));
Full code can be found here : http://pastebin.com/mhKi1sKd
What is the best way to optimise the imageView so that in case multiple images are shown in a RecyclerView, Heap memory does not get full and also if there are unnecessary codes in my program that simply take up processing and valuable time?
This is what I have achieved till now.
Have a look at Picasso library.
Many common pitfalls of image loading on Android are handled
automatically by Picasso:
Handling ImageView recycling and download cancelation in an adapter.
Complex image transformations with minimal memory use.
Automatic memory and disk caching.

How do I get my ImageButton to resize correctly for an asset image?

My app has an Activity that displays a vertically scrolling list of ImageButtons. I want each buttons image to (A) come from the assets folder and (B) retain it's aspect ratio as it scales. Unfortunately, my ImageButton doesn't size correctly when it's image comes from the assets folder.
ImageButton src set from drawable
The first screenshot is my test app where the images all come from my apps drawables. That's the "correct" aspect ratio for that image, which is what I want to keep (all of the image buttons have been given scaleType "fitXY" "centerCrop").
ImageButton src set from assets
The second screenshot is my test app where the images all come from my apps "assets" folder — as you can see, the images are stretched out to the full width of the screen as desired, but the original aspect ratio has been lost:
Activity code (MainActivity.java)
LinearLayout buttons = (LinearLayout) findViewById(R.id.buttons);
View button = layoutInflater.inflate(R.layout.snippet_imagebutton, null);
ImageButton imageButton = (ImageButton) button.findViewById(R.id.imageButton);
if (GET_IMAGE_FROM_ASSETS) {
InputStream s = assetManager.open("image.png");
Bitmap bmp = BitmapFactory.decodeStream(s);
imageButton.setImageBitmap(bmp);
} else {
imageButton.setImageResource(R.drawable.image);
}
TextView buttonText = (TextView) button.findViewById(R.id.textView);
buttonText.setText("Button text!");
buttons.addView(button,
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
Button layout (snippet_imagebutton.xml)
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<ImageButton
android:id="#+id/imageButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" />
<TextView
android:id="#+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:gravity="center"
android:textColor="#FFF"
android:background="#88000000"
android:textAppearance="?android:attr/textAppearanceMedium" />
</RelativeLayout>
Dodgy hack
I've found a dodgy hack which achieves what I want and illustrates the problem — the ImageButton will size correctly to match a scaled up image from the resources, but it won't size correctly to match a scaled up image from the assets. I'd prefer a "real" solution over the dodgy hack if there is one. :)
// to start with, scale the ImageButton based on an image in our
// resources that has the same dimensions as the image in our assets
imageButton.setImageResource(R.drawable.image);
// when we eventually render (we don't have width / height yet)...
imageButton.post(new Runnable() {
public void run() {
int height = imageButton.getHeight();
int width = imageButton.getWidth();
// replace our "resource" image with our "asset" image
imageButton.setImageBitmap(bmp);
// and force the ImageButton to keep the same scale as previously
imageButton.setLayoutParams(
new RelativeLayout.LayoutParams(width, height));
}
});
Summary
I want to get the images for these buttons from my apps "assets" folder. How do I fix my app so that the button images are all scaled properly (i.e. retain their original aspect ratio)?
I'm assuming this has something to do with the framework not actually knowing the width / height of the image before it renders the ImageButton to screen — so how do I fix that? I tried setting adjustViewBounds to "true" on both the RelativeLayout and the ImageButton itself, but that didn't seem to have any effect.
The aspect ratio is not kept when you give the scale type attribute as "fitXY". Try "center", "center_crop" or "center_inside" according to your needs. Check ImageViewScaleType as well.
From what I gather, AdjustViewBounds is actually the method that provides the exact functionality you need. However, as Roman Nurik explains here, AdjustViewBounds does NOT increase the ViewBounds beyond the natural dimensions of the drawable.
Without seeing how you allocated resources/assets, my suspicion would be that this is why you see a difference when loading from Resources and when loading from Assets. Your resource file is - presumably - being scaled up when you load it (since that is the default behavior for resources), so that the actual size of the drawable returned from /res is significantly larger than the bitmap you decode from /assets.
Overriding ImageButton (as Nurik suggests), is of course one option, but probably overkill.
Assuming I am correct about the problem, you should be able to fix it simply by setting your BitmapFactory.options correctly when you load the Assets file. You need to set inDensity and inTargetDensity correctly according to your device (get DisplayMetrics), and set inScaled to true.
Alternatively, you can always rescale the bitmap manually before loading it on the ImageButton. Just grab the screen width to determine how large you need to make it.
P.S.
Auto-scaling is obviously the elegant solution, but it does have a weakness (which applies equally to both resources and assets) - if the width of your display > bitmap witdh when scaled up, you'll likely still have the aspect ratio messed up. You can check this pretty easily by going to landscape mode and seeing whether the resource file still result in the right aspect ratio. If you have a large enough base image (or use different layouts according to the screen size/orientation), then this should not be a problem though. Just something to keep in mind.

Images of same dimensions are sized differently by ImageView

I have a frame layout encompassing an imageview that's supposed to update the image it's showing when the user presses the next button (think of a slideshow). All the images that I want to thusly put in this imageview are of the same dimensions. The initial image that's shown by the imageview at the start of the activity is in the res/drawable-hdpi directory, while the other images live in the assets directory. The problem is that the images that I swap into the imageview appear smaller than the first image although all these images are of the same size.
With regard to layout parameters, my frame layout's layout_width and layout_height are set to wrap_content and so is the imageview, I tried setting the scaleType parameter to every possible value and I still see the same results. So unless the images stored in res/drawable-hdpi and assets are somehow scaled differently this behavior shouldn't happen.
So, what's the best approach to preserve the dimensions of the imageview as the user cycles through these images?
Here is my XML for the frame layout & my imageview
<FrameLayout
android:id="#+id/coverLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" >
<ImageView
android:id="#+id/coverImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="false"
android:scaleType="fitCenter"
android:src="#drawable/coverImage" />
</FrameLayout>
Here is my code that loads new images into my imageview
_coverImageView = (ImageView) findViewById(R.id.coverImageView);
...
...
if (loadNewImage) {
AssetManager assetMgr = getAssets();
InputStream stream;
try {
stream = assetMgr.open("covers/" + _coverImageName);
Drawable coverDrawable = Drawable.createFromStream(stream, _coverImageName);
_coverImageView.setImageDrawable(coverDrawable);
}
catch (IOException ex) {
System.out.println(ex);
}
}
So unless the images stored in res/drawable-hdpi and assets are somehow scaled differently this behavior shouldn't happen.
This is exactly the issue. Android scales bitmaps based on several rules including the density (class) of the screen and which resource folder the bitmap was from. See this related question/answer. If you want the same behavior for both assets and resources, you can put the initial cover image in drawable-nodpi instead of drawable-hdpi.

Replacing existing drwable in imageview is wrong scaled on some devices

The Situation:
I have a imageview with a layer list of two drwables.
The frame drwable is 800x400px and the cover drawable is 800x380px.
Both images reside in the folder drawable
<ImageView
android:id="#+id/preview_img_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="0.9"
android:adjustViewBounds="true"
android:src="#drawable/result_preview"/>
The Drwable layer list in drawable/result_preview
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/cover">
<bitmap
android:gravity="left|top"
android:src="#drawable/cover" />
</item>
<item
android:id="#+id/frame">
<bitmap
android:gravity="left|top"
android:src="#drawable/frame" />
</item>
</layer-list>
This setup works as expected the cover is displayed framed on all devices. Now the user can replace the sample cover with another cover of the same size.
Replacing the cover in the layer list and leave the frame as is.
LayerDrawable layer = (LayerDrawable) getResources().getDrawable(R.drawable.result_preview);
InputStream coverIs = getContentResolver().openInputStream(Uri.parse(coverUri));
this.drwCover = (BitmapDrawable) BitmapDrawable.createFromStream(coverIs, coverUri);
drwCover.setGravity(Gravity.LEFT | Gravity.TOP);
//drwCover.getBitmap().setDensity(160);
layer.setDrawableByLayerId(R.id.cover, drwCover);
imageView.setImageDrawable(layer);
The Problem:
Replacing the current cover with a new one (same dimensions as old) produces different results depending on the Device used.
On a device with a 3.2 screen and a 480x320 resolution the cover replaced fits in the frame. On a device with a 3.7 and a 800x480 resolution the replaced cover is displayed smaller then the old one.
What I found out is that the drwable in the imageview on the small device has a intrinsic height of 800x400 same as the dimensions of the drawable.
On the bigger screen the intrinsic height of the drawable is 30% bigger.
I know the the intrinsic values may differ from screen to screen.
What I was expecting is that the drawable that replaces the old one should will be scaled up the same way the old one was to +30%. But this did not happen.
Question:
Is there a option to tell the imageview or the layer list to Adpt itself? I think there should be a way to do so because the system did it already at the beginning.
First, I'd suggest doing the cover/frame differently:
make your frame a nine-patch drawable, so you can define in the nine-patch the padding that will remain visible when the cover is drawn on top of it.
put the frame drawable as a background
set the cover as the src of the image, and not the layer list
don't forget to set a scaleType for your ImageView, play with the different options, so suit your needs.
<ImageView
android:id="#+id/preview_img_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="0.9"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:background="#drawable/frame"
android:src="#drawable/cover"/>
For other having the same problem. 9patch png is a solution to a problem I don't have.
So far the only thing that came close to solve the problem was to put my resources into the folder drawable-nodpi. Here is the reference to the docs

Categories

Resources