ImageView: automatically recycle bitmap if ImageView is not visible (within ScrollView) - android

So, I've been looking at the ImageView source for a while, but I haven't figured out a hook to do this yet.
The problem: having, let's say, 30 400x800 images inside a ScrollView (the number of images is variable). Since they fit exactly in the screen they will take 1.3 MB of RAM each.
What I want: to be able to load/unload bitmaps for the ImageViews that are currently visible inside a ScrollView. If the user scrolls and the bitmap is no longer visible (within a distance threshold) the bitmap should be recycled so that the memory can be used by other bitmaps in the same ScrollView. I'm doing downsampling and all that, so no worries there. Bonus points if you do this by only extending ImageView (I would like to not mess with the ScrollView if possible).
Summary: I can make the images load only after the ImageView becomes visible (using a clever trick), but I don't know when to unload them.
Notes: I can't do this with a ListView due to other usability reasons.

Call this method when you want to recycle your bitmaps.
public static void recycleImagesFromView(View view) {
if(view instanceof ImageView)
{
Drawable drawable = ((ImageView)view).getDrawable();
if(drawable instanceof BitmapDrawable)
{
BitmapDrawable bitmapDrawable = (BitmapDrawable)drawable;
bitmapDrawable.getBitmap().recycle();
}
}
if (view instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
recycleImagesFromView(((ViewGroup) view).getChildAt(i));
}
}
}

Have a look at the android documentation about "How to draw bitmaps" Also the code example called bitmap fun.
If you use a ListView or a GridView the ImageView objects are getting recycled and the actual bitmap gets released to be cleaned up by the GC.
You also want to resize the original images to the screen size and cache them on disk and/or RAM. This will safe you a lot of space and the actual size should get down to a couple of hundert KB. You can also try to display the images in half the resolution of your display. The result should still be good while using much less RAM.

What you really want is to use an ArrayAdapter for this. If, like your notes suggest, that is not possible (although i don't see why) take a look at the source for array adapter and rewrite one to your own needs.

Related

Memory issues drawing 5 small images

My app is using way too much memory, and I'm not too sure why. I'm making a solitaire game. I'm using a chained ConstraintLayout of 10 equal width RelativeLayouts (1 for each stack of cards). [I know I can use other Viewgroups].
My app is using ~15mb memory, until I do this;
public void DrawCardsMat0()
{
RelativeLayout mat = (RelativeLayout) findViewById(R.id.PLAY_Mat0);
mat.removeAllViews();
RelativeLayout rl = new RelativeLayout(this);
for (PlayingCard playingCard : Stack0.Cards)
{
ImageView myImg = GetCardImage(playingCard);
RelativeLayout.LayoutParams lay = new RelativeLayout.LayoutParams(CardWidth, CardHeight);
lay.setMargins(0, playingCard.UI_MarginTop, 0, 0);
mat.addView(myImg, lay);
}
}
and
public ImageView GetCardImage(PlayingCard playingCard)
{
ImageView myImg = new ImageView(this);
// a simple switch is used to here to pick the drawable. Removed as there's 52 cards)
myImg.setImageResource(R.drawable.s1);
return myImg;
}
CardWidth and CardHeight are ints (150 and 210, roughly. Pixels).
This draws FIVE cards, and each .jpg is ~60KB (yes, KB). This results in 60MB of memory usage. 10 cols == 600mb of memory (I assume, as it's OOM before then).
The images are in a RelativeLayout, and are placed on top of each other. MarginTop is used to "stack" them.
thanks in advance
--- update 1 ---
I'm now using a 46KB bmp image.
Creating 5 ImageViews (one stack's worth) in XML (and don't call anything in code) uses ~50mb memory. Tested using a LinearLayout.
Creating same images in-code uses same amount of memory.
--- update 2 ---
If I create a new Activity, with just a simple LinearLayout and 5 ImageViews, and no java code, it still uses ~50MB memory.
When working with bitmaps, you should always be careful. I'll describe the most important points and considering them, you can change the way work with card images is organized.
Bitmap in memory always exists in uncompressed format, so its size will be different from the size of your .jpg/.png/.webp/.[whatever compressed format]. The actual size depends on the Bitmap.Config, but by default Android will use ARGB_8888 (1 byte for alpha channel, 1 byte for each of R/G/B channels). You can roughly estimate the size in memory by saving the image in .bmp format.
When you use <bitmap> tag with drawable, it means in runtime bitmap will be wrapped in BitmapDrawable. As you can see, by default bitmap will be loaded as-is, taking original size.
When you call myImg.setImageResource(R.drawable.s1) it means that bitmap will have the same size as before in memory, it will be just scaled by using the matrix depending on the size of ImageView and scaleType you specified.
Considering that, probably you may want to load downscaled bitmap to the memory, depending on the view size. You can do that by using BitmapFactory class instead. I suggest you to read these 2 articles in order to understand how Android is working with bitmaps: Managing Bitmap Memory and Loading Large Bitmaps Efficiently.
You may also consider using some image library, like Picasso or Glide which can load downscaled bitmap depending on the size of ImageView. You can also try to use Fresco which keeps bitmap outside of JVM heap.
If you have such possibility, better to have cards in SVG format. Then you can work with vectors instead of having bitmap, which is much more memory-friendly (because SVG is just a set of instructions on how to draw). However in case of really complex SVG file you can hit another problem - it will take a lot of time to draw it. Check this article on how to use it with ImageView.

Android Memory Leak - setBackgroundDrawable - with images

When a user clicks a button, i keep switching the background of a layout like this in Activity code:
...
mylayout.setBackgroundDrawable(getResources().getDrawable(R.drawable.img1));//On First click
mylayout.setBackgroundDrawable(getResources().getDrawable(R.drawable.img2));//On Second Click
mylayout.setBackgroundDrawable(getResources().getDrawable(R.drawable.img10));//On 10th Click
...
mylayout.setBackgroundDrawable(getResources().getDrawable(R.drawable.img1));//On 11th Click 1st image again and so on.
I have 10 images which i keep rotating.
Soon, it causes OutOfMemory exception. What am i doing wrong?
If it matters in my manifest file i have:
android:minSdkVersion="11"
android:targetSdkVersion="17" />
EDIT 1
Average size of the image is: 50K
Average dimension of the image is: 600x450
All images img1, img2 etc.. are jpeg images
Solution Update
Reducing image dimensions to 300x200 resolved the issue. The memory requirements went down significantly by this single change.
It might be happening because your image resources are in drawable folder; which is equivalent to drawable-mdpi. And your device might be something other than mdpi.
So, either provide images for all the screed densities you are gonna support. Or, put images in drawable-nodpi, so your images will not be resized. Hence no OutOfMemoryException.
Referred from here
Use Bitmap.recycle() if you can change your Drawable with Bitmap.
Hope This will help
Each bitmap with dimension 600x450 has size of 600*450*4 = 1 080 000 bytes (1 MB) in RAM.
You should load so large bitmaps in another way:
Bitmap bitmap = BitmapFactory.decodeResource(resources, R.drawable.img1);
myLayout.setBackgroundDrawable(new BitmapDrawable(resources, bitmap));
And before changing background of the view you have to do
Drawable background = myLayout.getBackground();
if (background != null && background instanceof BitmapDrawable) {
myLayout.setBackgroundDrawable(null);
BitmapDrawable bd = (BitmapDrawable) background;
bd.getBitmap().recycle();
}
I dont think changing background layout so frequently is a good idead. I suggest that you could use RelativeLayout with an ImageView as first item. Then everytime you need to change your background, you can change your ImageView with some cached library such as: Universal Image Loader

Viewpager with large bitmaps

Hi I'm trying to build something like an horizontal gallery where I can add or remove pages using images from my gallery/camera.
I'm trying to make it work with large bitmaps so I'm using an algorithm to scale the bitmap and set it to the imageview of each page.
The algorithm requires the width/height of the ImageView (to scale down).
The problem I have is that when my custom PagerAdapter method is executed, the width/height of the ImageView is not yet known (getWidth/getHeight return 0), so it does not work:
public Object instantiateItem(ViewGroup collection, int position) {
LayoutInflater inflater = (LayoutInflater) collection.getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.document_page, null);
((ViewPager) collection).addView(view, 0);
ImageView imageView = (ImageView) view.findViewById(R.id.imageView);
// Obtain the image file URI
// Call algorithm to get scaled bitmap using ImageView width and height --> PROBLEM: imageView.getWidth()/Height() return 0!!
// Set ImageView with scaled bitmap to avoid OutOfMemory Exception
return view;
}
What do you suggest?
Thanks.
Scaling the image client-side:
You don't need your own algorithm to scale an image if it's already on your device. An ImageView has a ScaleType that you can set like this:
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP)
(or in XML with the android:scaleType attribute).
The remaining question is how to get the image from your URL to display in the ImageView. Use libraries such as SmartImageView or Android-Query (more powerful) to achieve that in an asynchronous manner (i.e. without blocking your UI thread).
With SmartImageView, for example, you are able to use something like this:
myImageView.setImageUrl("http://www.awesomeimages.com/myawesomeimage.jpg");
Just read the examples on that page, there are far more options.
Scaling server-side
If you have some kind of resize algorithm that is triggered e.g. by certain URL parameters, use this solution and pass the width and height as you desire (maybe with one of the above-mentioned libraries).
As you said that getWidth() and getHeight() return 0, please look at the layout_width and layout_height that you have set for your ImageView. If it's fill_parent, there should be no problem, but if it has to be wrap_content, consider using a local, transparent dummy image with same aspect ratio as your final image (until the "real image" has been set).
Use bitmap factory with justInbounds option. So it will just return the dimension of bitmap without loading bitmap to the ram.
options.inJustDecodeBounds = true;
try to sample the bitmap to the size of the screen (use inJustDecodeBounds in order to get the resolution of the bitmap , and displayMatrics in order to get the resolution of the screen ) .
if there is no transparency , you might also wish to use the RGB 565 format of bitmap decoding instead of 8888 . this will save about 1/4 of the memory being used .
in any case , i suggest reading this tutorial of handling bitmaps .
I was facing the same issue, but I came up with a solution: create a function and call it from "public Object instantiateItem(ViewGroup collection, int position)".
The trick is: the created function will be set to post execute after some miliseconds (to do that you will use the Handler object)
Doing that way the "instantiateItem" will be already done, and view elements will have widths and heights values set.
Bellow is some sample code on how to proceed:
#Override
public Object instantiateItem(ViewGroup container, int position)
{
View view = inflater.inflate(R.layout.pager_list_item,container,false);
TextView textView= (TextView) view.findViewById(R.id.pagerTextView);
ImageView imageView = (ImageView)view.findViewById(R.id.pagerImageView);
... //stuff that don't need the width and height
Handler handle = new Handler();
handle.postDelayed(new Runnable()
{
//here the widths and heights values should be already set
//now you can test the view.getWidth() and view.getHeight()
if(view.getWidth()!=0 && view.getHeight()!=0)
{
//now you sare safe to call the android algorithm for scaling large bitmaps
}
}, 100); //it will wait 100 miliseconds to execute
return view;
}
100 miliseconds is an arbitrary number, maybe too much!!
I recommend you to use a loop or recursive function to guarantee that the block inside the "if" statement will be executed, that way you can use even smaller miliseconds.
In my project I used a recursive function that called itself until the "if" block is executed, and I set to 5 miliseconds.
It should work without a loop or recursive methods, just by setting an arbitrary number like 100 and testing later, but you can't assure that the app will behave the same for all devices, for one can be fast and for other may take a little longer for view to be ready, you never know.

android background image slows down app

I am using a viewpager to swipe amongst fragments in my app. I define the background in the XML, so
android:background="#drawable/bg_final"
If I use a simple background color, my app works very smooth. If I use it with this background image, fps decreases and my app becomes only passable. It is not slow, just not so smooth, however on weaker devices it could work laggy. Is it a bad way to apply a background image? The whole image is a 480x800 png with the size of 14.7kB. What might be the problem?
(the backgrounds of the fragments are transparent, the viewpager is in a main.xml which has its background with this image)
There are lots of answers out there that point to pre-scaling your bitmap. This is very imporant and you should do it if you can, however after trying several different things, the biggest performance gain I experienced was to create a view and override the onDraw method.
The class could be as simple as this:
public class DTImageView extends View {
public Bitmap imageBitmap;
public DTImageView(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(imageBitmap != null)
canvas.drawBitmap(imageBitmap, 0, 0, null);
}
}
Invoke the code using a prescaled bitmap:
DTImageView bgImageView = new DTImageView(context);
bgImageView.imageBitmap = Bitmap.createScaledBitmap(bitmap,width,height,true);
At this point you can add this view to the view hierarchy where it belongs. In my case, my view hierarchy had a bgImage, a middle ground where all the other functionality happened, and fgImage.
Use the android sdk tool Hierarchy Viewer to profile the app and see how fast each view is taking to draw. My app spent 30-40ms drawing the bgImage and fgImage. Pre-scaling and overriding onDraw reduced that 30-40ms to about 1.5ms.
Here is a screenshot of the Draw performance for one of my views before and after:
BEFORE:
AFTER:
I'm a bit late, but this just bit me, and when running the GPU debugger, I noticed that the system scaled up my image! I had a 1920x1080 image, and it showed up as a 3780x6720 texture!
Ideally, you should have textures for each density in the respective folder (res/drawable-mdpi, res/drawable-hdpi, etc).
However, if you think your one texture is fine, put it into res/drawable-nodpi, not res/drawable. If you put it into res/drawable-nodpi, the system won't scale it up based on the dpi.
In my case, it changed a simple UI from 10fps on a Pixel XL to the full frame rate of 60fps.
I had a background image, not big in size, but with weird dimensions - therefore the stretching and bad performance. I made a method with parameters Context, a View and a drawable ID(int) that will match the device screen size. Use this in e.g a Fragments onCreateView to set the background.
public void setBackground(Context context, View view, int drawableId){
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), drawableId);
int width = Resources.getSystem().getDisplayMetrics().widthPixels;
int height = Resources.getSystem().getDisplayMetrics().heightPixels;
bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
BitmapDrawable bitmapDrawable = new BitmapDrawable(context.getResources(), bitmap);
view.setBackground(bitmapDrawable);
}
The reason for this could be that the image is being stretched, and Android has performance issues with stretching background images.
Instead, you should either replace the background image with either a background color or see this answer to have the image repeat instead of stretch.
I had the same problem and I resolved it by using this method:
Add your image to the mipmap folder which is located under the res directory. It will add the image according to it's density.
Create add only one background image with size 1080x1920 and put it in drawable-nodpi folder and that solved this problem

Creating ImageViews for a Gallery that fit the Gallery's height in Android

I want to create a Gallery that displays a very large number of images. To do this I have created an adapter class with an ImageView[3] and I making getView pass out ImageView[position%3]. I am also setting the adapter to populate the ImageView with large bitmaps that are loaded in asynchronously.
Since a fling may scroll faster than the device can load in images, I also have a much longer array of thumbnails (which I will also be eventually using for a thumbnail Gallery). If the large bitmap is unavailable, the ImageView is populated with the thumbnail, then repopulated with the large bitmap once it has loaded.
My problem is therefore depressingly trivial: the ImageView is not scaling the way I want it to. The desirable behaviour is for the ImageView to be the same height as its parent, but have the same aspect ratio as its content. However, if I populate the ImageView[] using this code:
private void initializeStaticArrays() {
//initialize envelope arrays
for(int i=0; i<ENVELOPE_ARRAY_SIZE; ++i)
{
mEnvelopeViews[i] = new ImageView(mContext);
//make the views as wide as their content and as tall as their parent
mEnvelopeViews[i].setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
//then make them scale the contained image
mEnvelopeViews[i].setScaleType(ImageView.ScaleType.FIT_CENTER);
mEnvelopeIds[i] = Gallery.INVALID_POSITION;
mBundles[i] = new BitmapFilePathBundle();
}
for(int i=0; i<THUMBNAIL_ARRAY_SIZE; ++i)
{
mThumbnailViews[i] = new ImageView(mContext);
if(mThumbnailParams!=null) mThumbnailViews[i].setLayoutParams(new Gallery.LayoutParams(mThumbnailParams));
mThumbnailViews[i].setScaleType(ImageView.ScaleType.FIT_CENTER);
mThumbnailIds[i] = Gallery.INVALID_POSITION;
}
}
as soon as the Gallery calls getView() it crashes (please note I have called setImageBitmap elsewhere in code and that has definitely happened before getView() is called). I would imagine this is an arithmetic loop, something like trying to make the bitmap stretch to fit the view, which is trying to contain the bitmap, but I can't work out what combination of layout parameters would give the result I want.
So: is there a way to make an ImageView as tall as its parent, with its content zoomed to fit it with the correct aspect ratio, and with the width of the ImageView exactly matching the width of the content?
Edit: the crash was caused by a CastException from LayoutParams to Gallery.LayoutParams. Changing the appropriate line to this:
//make the views as wide as their content and as tall as their parent
mEnvelopeViews[i].setLayoutParams(new Gallery.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
fixes the crash but still does not fix the zoom problem.
Unfortunately, in the absence of any automatic scaling solutions to this problem, I ended up manually sizing the views to specific dimensions, and then using Matrix objects to determine how each image fit those dimensions.

Categories

Resources