The default Gallery widget on Android does not recycle views - everytime the view for a new position is called the widget always calls the getView
method of the adapter with convertView
set to null.
As you scroll backwards and forwards this ends up in lots of views being created, which the recycler component that the Gallery stores them in does not seem to recycle them quickly enough leading to an OOM situation.
You can test this easily with a few large-ish images as your gallery items, but just a TextView will cause it in the end. Put a log statement with a counter in the getView
method of your adapter also to see how many new views are created.
Does a third-party widget which behaves like a Gallery but that also implements view recycling exist?
My solution was, in the end, going with @CommonsWare's suggestion to modify the Gallery source code. This is also required copying the following files:
but these are pretty simple.
After that I modified code to do the following:
RecycleBin
(AbsSpinner
)
- Place objects in the recycler one after another, rather than according
to position
- Retrieve objects from the bottom of the recycler, regardless of the
position requested
- The existing implementation assumed that each different position in the adapter
resulted in a unique view. The changes above are only good if your Gallery contains
only one type of item, if not you'll need to add some sort of key based on item type
and the amount of that type required
Gallery
- Used reflection (ugh) to modify the private
mGroupFlags
variable of ViewGroup
to allow child re-ordering - I also set a boolean value indicating whether the field access succeeded which I test before using the component.
- Removed all calls to
mRecycler.clear()
- The number of
items the gallery has to display
changes as it scrolls and the existing
implementation would clear the
recycler when (a) setSelection was
called (b) a motion scroll occurred
With these modifications my counter in my newView
method in my adapter reached... 7.
Here is the code (Placed in the public domain 2013/08/07 under http://en.wikipedia.org/wiki/WTFPL)
Actually there is an alternative, though I haven't personally tested it:
https://github.com/falnatsheh/EcoGallery
I used patch from http://code.google.com/p/android/issues/detail?id=3376#c19
Super late to the party, but I've modified EcoGallery to do a few more things (and avoid some crashes).
I've called it TimelineGallery and it's the same crap as the Gallery, but it can do smooth scroll and doesn't do weird stuff when images are loaded asynchronously.
To demonstrate it, the sample uses Picasso and PullToRefresh.
The original code, copyright and such belongs to Google so blame them for making such a crappy widget.
Final note: I do not recommend using the gallery, it's old, buggy, hacky and will probably never be maintained. The problem is not about fixing its bugs, the problem is that the whole architecture of the Gallery is wrong and as such, fixing it is not possible without introducing more hacks.
Google realized this and deprecated it. Use a ViewPager or a HorizontalScrollList and deal with the limitations of each.
If you still want to go ahead and use this "gallery", feel free, it works, but it may crash your app and may frustrate you.
Another quicker WorkAround for the OutOfMemory Issues, is to try/catch the code where you decode the image and if the OutOfMemory-exception is thrown, you try to decode it with smaller Resolution again..
something like this:
private static Bitmap decodeFile(File f, int size, int suggestedScale) {
int scale = 1;
Bitmap bmp = null;
try {
// Decode image size
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
BitmapFactory.decodeStream(new FileInputStream(f), null, o);
// Find the correct scale value. It should be the power of 2.
int width_tmp = o.outWidth, height_tmp = o.outHeight;
if(suggestedScale > 0)
scale = suggestedScale;
else {
if (width_tmp >= height_tmp) {
scale = Math.round((float)(width_tmp) / size);
} else {
scale = Math.round((float)(height_tmp) / size);
}
}
if(scale < 2)
return BitmapFactory.decodeFile(f.getPath());
Debug.i(TAG, "width: " + width_tmp + " height: " + height_tmp + " scale: " + scale);
// Decode with inSampleSize
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize = scale;
bmp = BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
} catch (FileNotFoundException e) {
} catch(OutOfMemoryError e) {
Debug.i(TAG, "we retry it cause of an OutOfMemoryException");
return decodeFile(f, size, scale+1);
} catch(Exception e){
Debug.w(TAG, e);
}
return bmp;
}
Ofcourse it is now possible, that you will see different resolutions of the same picture at different times - But at least your Gallery will not crash anymore and you allways show the highest resolution you can.