I'm looking for ideas on how to optimize the following code which is the body of a for loop:
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
options.inJustDecodeBounds = false;
ImageButton view = new ImageButton(this);
view.setBackgroundColor(0);
view.setOnClickListener(new OnClickListener(){
#Override
public void onClick(View v) {
finalThis.onBorderClicked(v);
}
});
view.setTag(i); //i is the loop counter
Bitmap big = BitmapFactory.decodeResource(getResources(), borders[i], options);
Bitmap smaller = Bitmap.createScaledBitmap(big, borderDimension, borderDimension, false);
view.setImageBitmap(smaller);
view.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.FILL_PARENT));
((LinearLayout.LayoutParams)view.getLayoutParams()).setMargins(0, 0, -5, 0);
group.addView(view);
big.recycle();
Basically I've got images that are 600x600 and I want to get them to size of around 50dp. So naturally, I first set the sample size to 4 (600 / 4 = 150), so that I don't occupy unnecessary memory and then further resize the image down to the specified size. I then use it to load it onto an ImageButton, and also I recycle the bigger, now useless Bitmap.
This is a dynamic UI creation code, the ImageButtons are added to a HorizontalScrollView. Thing is, in one case, I've got to add more than hundred ImageButtons to the UI and this code is awfully slow.
The question is: how can I make it work faster? It is currently executed on the MainActivity, could multithreading (perhaps AsyncTask) help in this instance? And is there any other way that would possibly be faster for getting the Bitmaps to the desired size? If there is no way to make this process significantly faster, then is there a way to progressively add the ImageButtons one by one (but in the same order) so that the UI is not frozen when all of this happens?
Taking your code off the main thread will not make the application process the images any faster, but it will keep the UI responsive to the user while that process is taking place and is absolutely a necessary step when you have this much data to load at once. However, keep in mind that you cannot manipulate the view hierarchy from any thread other than the main thread, so you cannot just place this whole operation inside of an AsyncTask.doInBackground(). You will have to split out the lines that take the most time (i.e. your decodeResource() and createScaledBitmap() calls), but the code to instantiate and add views to the parent will need to happen in onPostExecute() or publishProgress() (depending on how you implement the task logic).
Thing is, in one case, I've got to add more than hundred ImageButtons to the UI and this code is awfully slow.
Another important thing to keep in mind is that having upwards of 100 image-based views in your hierarchy at any one time is still a large chunk wasted memory. The best way to speed up the process is not to add all the views at once, but only add them as they are needed (i.e. when the user scrolls over) and then remove/recycle views that are no longer on screen. This is how AdapterView implementations work for this very reason, use only the memory you need and your app will be both faster and more efficient.
I realize there is no HorizontalListView in the framework, but there are 3rd party implementations that provide this functionality for you, or you can look at the source for one of the framework classes to build your own from.
You don't need to scale the image by yourself. You can do this by the asking the ImageButton to scale for you, which is much faster.
ImageButton view = new ImageButton(this);
view.setBackgroundColor(0);
view.setOnClickListener(new OnClickListener(){
#Override
public void onClick(View v) {
finalThis.onBorderClicked(v);
}
});
view.setTag(i); //i is the loop counter
view.setAdjustViewBounds(true);
view.setMaxHeight(borderDimension);
view.setMaxWidth(borderDimension);
view.setImageResource(border[i]);
view.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
((LinearLayout.LayoutParams)view.getLayoutParams()).setMargins(0, 0, -5, 0);
group.addView(view);
I have changed the layout params to use WRAP_CONTENT for height, because I do not know how does your image look like. You can play around with the layout a bit, but this generally should do the trick.
I found a good solution at http://developer.android.com/training/displaying-bitmaps/index.html
It has following code, which dynamically set Bitmap to ImageView.
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
#Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// Once complete, see if ImageView is still around and set bitmap.
#Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
You can call this by using,
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
Here, just change ImageView to ImageButton.
Edit:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = 4;
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
You can find more about handling Bitmaps Efficiently here
Related
I have searched for hours on the subject of memory leaks and I cant seem to solve my problem. After exactly 9 rotations my app crashes with an OutOfMemory error. I have both Bitmaps and Drawables with Bitmaps in my app. I have tried putting in code that removes callbacks to drawables, I have tried setting all Bitmaps to null as well as manually calling the garbage collector. I had all of this code in the onSaveInstanceState() method since I assume that is called whenever the screen changes and before destruction of the view. None of these solutions worked.
I got some results with the Bitmaps turned to null, however that only added about another 9 screen rotations before another memory leak. Where is my leak? What am I doing wrong? I was under the impression that when a screen is rotated, everything is destroyed and recreated, that must obviously be false.
I dont want to post my code because A. there is a lot of it and B. its close to finishing and so becomes a company secret per se. If there is something you must absolutely see to solve this then I will post it. I think I just need a really good explanation of what is actually going on when the screen is rotated and how to correctly handle bitmaps and drawables. Note: This error does not pop up if I leave the app and then go back in, it only happens when the view is resized upon screen rotation.
I have about 7 bitmaps and 7 drawables, all created at launch and resized when the screen rotates. DOing that several times stops the app.
Some simplified code:
How I set up a Bitmap to a drawable. This one sets to a ClipDrawable as well:
//Full Bar
colorbarMap = BitmapFactory.decodeResource(res, R.drawable.colorbar);
colorbarDraw = new BitmapDrawable(res, colorbarMap);
colorbarDraw.setBounds(barX, barY, barX2, barY2);
colorbarClip = new ClipDrawable(colorbarDraw, Gravity.BOTTOM, ClipDrawable.VERTICAL);
colorbarClip.setBounds(barX, barY, barX2, barY2);
colorbarClip.setLevel(currentLevel);
//Empty Bar
colorbaremptyMap = BitmapFactory.decodeResource(res, R.drawable.colorempty);
colorbaremptyDraw = new BitmapDrawable(res, colorbaremptyMap);
colorbaremptyDraw.setBounds(barX, barY, barX2, barY2);
The above code runs once at the start of the view initializing based on this code:
private void init(){
//System
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB) {
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
res = getResources();
options = new BitmapFactory.Options();
viewTreeObserver = getViewTreeObserver();
if (viewTreeObserver.isAlive()) {
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
view_width = getWidth();
view_height = getHeight();
afterLayout();
}
});
}
}
The first code snippet runs under the method: afterLayout()
Nothing is done with the bitmaps after this. Each bitmap is initialized with an x and y location based on the view width and height. That location is edited using a rectangle to set its bounds for example a moving object.
If the Bitmap that to be displayed is small enough than use this or you don't need a high res image
bitmapOption = new BitmapFactory.Options();
bitmapOption.inScaled = true;
bitmapOption.inSampleSize = 2;
imageBitmap = BitmapFactory.decodeFile(imgFile.getAbsolutePath(), bitmapOption);
this will reduce the amount of size of bitmap see the bald guys explanation
optimizing bitmap
put your bitmap loading code in try finally block and use this at the end
try {
...
}finally {
if (imageBitmap != null) {
//recycle the bitmap to improve performance and avoid OO Exception
imageBitmap.recycle();
imageBitmap = null;
}
}
This will recycler the bitmap space taken by the image whenever GC is called
Background
I have a ViewPager (of images pages), which needs to show its content inside an image that should keep its aspect ratio.
The problem
Problem is, when I just set a 9-patch for any kind of ViewGroup (including the ViewPager), the image just stretches (not keeping the aspect-ratio), and for some reason, it doesn't even respect the content bounding box I've set to it (in the 9-patch).
It occurs even if I put the ViewPager in a FrameLayout that has the 9-patch background instead.
What I've tried
I've tried to overcome this by not using 9-patch at all. I used a RelativeLayout with ImageView (of the image) , and the ViewPager set to align exactly to the ImageView.
Of course, this wasn't enough, since the ImageView doesn't take the same space as what it shows, so I had to calculate the padding needed to fix it, based on the image size and the imageView size, and then set it on a FrameLayout that includes the ViewPager. This almost works, but it has issues too (not precise calculations, which could cause one pixel row to show/hide of the viewPager).
The question
Is it possible to set a background for ViewPager (or any other ViewGroup), that will keep its aspect ratio, yet also allow padding using 9-patch?
ok, so what I did is this:
set the 9-patch as a background to the parent of the ViewPager, or to the ViewPager itself.
right when you get the size of the view, calculate what should be the width or height of it, depending on the bitmap size and the restrictions you have, and set it to the view.
In my case, the restrictions were to fill it to the height of the parent, so what I needed is to calculate the needed width...
Weird thing, is that I still see artefacts. The bottom of the viewpager has about 1-2 pixels rows of the background instead of being of itself (the "hole" of the 9-patch is at the bottom of the image).
EDIT: fixed it too. Seems to be an issue with various screen pixels-densities. Here's a snippet:
public static void runJustBeforeBeingDrawn(final View view, final Runnable runnable) {
final OnPreDrawListener preDrawListener = new OnPreDrawListener() {
#Override
public boolean onPreDraw() {
view.getViewTreeObserver().removeOnPreDrawListener(this);
runnable.run();
return true;
}
};
view.getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}
public static BitmapFactory.Options getBitmapOptions(final Resources res, final int resId) {
final BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, bitmapOptions);
return bitmapOptions;
}
final Options bitmapOptions = getBitmapOptions(getResources(), R.drawable.nine_patch_image);
final View viewPagerContainer = ...;
runJustBeforeBeingDrawn(viewPagerContainer, new Runnable() {
#Override
public void run() {
int neededWidth = (int)Math.ceil((float) bitmapOptions.outWidth * viewPagerContainer.getHeight() / bitmapOptions.outHeight);
final LayoutParams layoutParams = viewPagerContainer.getLayoutParams();
layoutParams.width=neededWidth;
viewPagerContainer.setLayoutParams(layoutParams);
}
});
EDIT: Alternative to runJustBeforeBeingDrawn: https://stackoverflow.com/a/28136027/878126
I'm trying to load in part of a tall image into a scrollable imageview (imageview inside a scrollview).
It works with smaller images but if the images is bigger than 2048x2048 the app will crash due to open gl out of memory.
I can load part of the image with this:
private Bitmap decodeBitmapRegion(InputStream in, Rect region) {
BitmapRegionDecoder decoder = null;
try {
decoder = BitmapRegionDecoder.newInstance(in, false);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
region.right = decoder.getWidth();
Bitmap reg = decoder.decodeRegion(region, null);
return reg;
}
Where the rectangle can be something like (0,0,DisplayWidth,800).
That works in most cases since i'm using the imageview to let the user crop a part of the image and save it as a coveriamge.The imageview has the same width as the display and the height of 400 dp.
So the image the user saves will be the same width as the orginal but only 400 dp's tall.
Is there anyway to make it possible for the user to scroll all the way to the bottom of the orginal image?
How does webview load images that are bigger than 2048x2048?
Here's one thought.
Imagine your image broken into rows of bands. Use a ListView to display the bands. A ListView uses an adapter. In adapter.getView(), you use the decoder to load the appropriate band.
The ListView already does "recycling". So, it more-or-less only holds enough rows to fill the screen.
This technique should work well if your images aren't too wide. If they are, and it's not tall enough to occupy more than one screen height, you'll still have the same memory problems. A more complicated solution would be to use some sort of grid view, but then you'll have to do your own view recycling. Not too difficult, but harder than using ListView.
I managed to get the functionality I wanted. But i don't think this is the best solution?
I loaded the image into a webview and then i took a screenshot of the visible part of the webview. After that i can save that bitmap and do whatever i want to it.
But there must be a better way to do this than to use a webview to show the original image?
Anyway here is what I did:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.button1);
imgView = (ImageView) findViewById(R.id.imageview);
webView = (WebView) findViewById(R.id.webview);
webView.loadUrl("file:///android_asset/test.jpg");
button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
imgView.setImageBitmap(getBitmapForVisibleRegion(webView));
}
});
}
public static Bitmap getBitmapForVisibleRegion(WebView webview) {
Bitmap returnedBitmap = null;
webview.setDrawingCacheEnabled(true);
returnedBitmap = Bitmap.createBitmap(webview.getDrawingCache());
webview.setDrawingCacheEnabled(false);
return returnedBitmap;
}
My application allows users to take photos with their devices camera app, which are saved to an app-specific folder.
Then, users can choose to send any image they choose to one of their contacts, using their choice of their devices email app.
Sadly the "choose images to send" dialog takes a long time to load initially, and is very "choppy" (frequent noticable pauses) when scrolling. I suspect that this is because the images are very large — pictures taken with my camera are around 2.3MB each. The initial screen (15 images displayed) thus needs to pull around 34.5MB off disk before it can render.
GridView layout.xml
<!-- ... -->
<GridView
android:id="#+id/gvSelectImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:numColumns="3"
android:gravity="center" />
<!-- ... -->
Adapter getView()
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
File pic = pics[position];
View checkedImageView =
LayoutInflater.from(context).inflate(R.layout.checked_image,
parent, false);
final ImageView imageView =
(ImageView) checkedImageView.findViewById(R.id.checkableImage);
/* LOAD IMAGE TO DISPLAY */
//imageView.setImageURI(pic.toURI())); // "exceeds VM budget"
Bitmap image;
try {
int imageWidth = 100;
image = getBitmapFromFile(pic, imageWidth); // TODO: slooow...
} catch (IOException e) {
throw new RuntimeException(e);
}
imageView.setImageBitmap(image);
/* SET CHECKBOX STATE */
final CheckBox checkBoxView =
(CheckBox) checkedImageView.findViewById(R.id.checkBox);
checkBoxView.setChecked(isChecked[position]);
/* ATTACH HANDLERS */
checkBoxView.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
CheckableLocalImageAdapter.this.isChecked[position] =
checkBoxView.isChecked();
}
});
imageView.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
checkBoxView.setChecked(!checkBoxView.isChecked());
}
});
return checkedImageView;
}
getBitmapFromFile() is just adapted from the answer given at https://stackoverflow.com/a/823966/633626. I note that the code there calls decodeStream() twice and might be doubling my I/O, but I think the dialog will still be too slow and too jerky even if I halve the amount of time to takes to render.
My testing phone is a standard Samsung Galaxy S II if that's relevant. I suspect older devices will be even slower / choppier.
What's the best way to improve performance here? I'm guessing a combination of an AsyncTask to load each image (so that the dialog can load before all images are rendered) and some sort of caching (so that scrolling doesn't require rerendering images and isn't jerky), but I'm lost as to how to fit those pieces in with the rest of my puzzle.
You'll want to work with thumbnails instead. Loading the whole image from the SD card will always be slow. Only load the full version of the one(s) they really want to send.
This isn't to say that caching and async loading are a bad idea. I'd change the code to work with thumbnails first, recheck performance, and then go the async route. You can show a little spinner over the currently loading thumbnail to indicate that there's more to come.
Here's a built-in class to help you retrieve them.
Try loading your Images with a SampleSize into the GridView:
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 7;
Bitmap bitmap = BitmapFactory.decodeFile(f.getPath(), opts);
More Info about the Options here
And also you can swap the image loading into a Thread or AsyncTask.
I made another post earlier about this subject but I've since changed my code around a bit based on suggestions but the same problem exists. My images keep shifting around if I click other elements on screen.
Here is my code which I call by this:
new Thumbnailer(image_main, image_table).execute(image);
image_main is my imageView and image_table is the table that holds it.
private class Thumbnailer extends AsyncTask<String, Void, Bitmap> {
private ImageView imageView;
private TableLayout imageTable;
public Thumbnailer(ImageView imageView, TableLayout imageTable) {
this.imageView = imageView;
this.imageTable = imageTable;
}
#Override
protected void onPostExecute(Bitmap result) {
imageView.setImageBitmap(result);
imageView.setVisibility(View.VISIBLE);
imageTable.setVisibility(View.VISIBLE);
}
#Override
protected void onProgressUpdate(Void... progress) {
}
#Override
protected Bitmap doInBackground(String... params) {
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
BitmapFactory.decodeFile(params[0], o);
final int REQUIRED_SIZE=70;
//Find the correct scale value. It should be the power of 2.
int width_tmp=o.outWidth, height_tmp=o.outHeight;
int scale=1;
while(true){
if(width_tmp/2<REQUIRED_SIZE || height_tmp/2<REQUIRED_SIZE)
break;
width_tmp/=2;
height_tmp/=2;
scale++;
}
//Decode with inSampleSize
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize=scale;
return BitmapFactory.decodeFile(params[0], o2);
}
}
I had a similar problem. I would load images into a ListView via an AsyncTask (which might fetch the image via HTTP), and it seemed to load the wrong image for a split second before loading the correct one. It was as if Android was reusing the row before the HTTP request finished, and the AsyncTasks were kind of colliding.
The comments above confirmed that is what was happening.
I fixed it by placing the image path in the tag of the ImageView before the AsyncTask starts, and then checking that tag after the AsyncTask finishes but just before it calls setImageBitmap.
Add these lines:
final String somethingUnique = "put the image path or some other identifier here";
image_main.setTag(somethingUnique);
Before this line:
new Thumbnailer(image_main, image_table).execute(image);
Now, in onPostExecute, check the tag:
// if this does not match, then the view has probably been reused
if (((String)imageView.getTag()).equals(somethingUnique)) {
imageView.setImageBitmap(result);
}
Sounds like you're getting bitten by view recycling. That is, when you scroll a List in Android, it doesn't create a new row and instead reuses a row taken offscreen. In this case, that can result in multiple tasks trying to load an image into a single imageview. You effectively need to somehow be able to tell which load request is the most recent for a row and cancel the earlier ones/ignore their results, or just use a library that handles this for you (like droid-fu).
You could also just turn off view recycling in your Adapter (that's the part that uses the convertView argument in the getView method), but be aware this will make list scrolling slow and jittery, especially on older devices.
This Android Developer's blog post provides a reference project for this. Just change the http image download code in the project to your doInBackground code.