I was looking for a custom gallery for displaying and selecting images on an Android phone, and while I found the sample code here to be a good start it needed some work before it was useful.
Before starting, it is worth pointing out that the Android API supplies a perfectly useful image gallery, and the StackOverflow thread how to pick a image from gallery (SD Card) for my app in android? has everything you need to get plug it into your own application.
If you require customised behaviour it isn’t too hard to get something rough working, so go to the original thread and create the ImageThumbnailsActivity and ViewImage classes, then configure the AndroidManifest.xml, res/layout/main.xml and missing res/values/strings.xml files.
The layout specifies a single GridView to hold the images, and the main ImageThumbnailsActivity activity performs two main jobs. First it loads all existing thumbnails and provides the code necessary to interact with the thumbnail if it is selected, and it has a nested adapter to manage the display of the thumbnails using a Cursor position into the GridView.
There are quite a few comments in the original posting indicating that the code is not quite right, and if you run it on a phone with a decent history of photos you’ll get some unusual behaviour. The two main issues are that firstly there are far more thumbnails displayed than there are images stored, and secondly that the images displayed are repeated as you scroll through the gallery.
The first issue can be resolved using the StackOverflow thread here and involves initially loading the full images rather than the thumbnail, but associating with the thumbnail when displaying on the grid. This means that we don’t try to show a thumbnail for an image that no longer exists or is incorrect for some other reason.
The second issue is a little more involved, but related to the fact that the nested ImageAdapter class will attempt to reuse View instances if possible (in the getView method), and the code provided assumes that if a View is returned to be reused that it is correct, which it is not. The GridView only creates as many View instances as required to fill the screen. Once you scroll to a new section it asks if you want to reuse the instances that have scrolled off the screen. Obviously this is an acceptable reuse of resources, but the reused instances will be showing the thumbnail for a different picture and will need to be updated.
As a matter of style, I also made the following changes:
- Remove all of the System.gc() calls, they aren’t required.
- close cursors
- make variables local where applicable
- as stated we load images from the MediaStore.Images.Media rather than the MediaStore.Images.Thumbnails but return the thumbnail for display.
- specify the MediaStore.Images.Thumbnails.MICRO_KIND for display. The benefit here is that the images are the size we intend to show and eliminited scaling. Feel free to play with settings but I found this the most pleasing layout and matches the existing Android gallery
Finally, if you look up the thumbnail when the View is visible on the screen, the scrolling is slow and limited to a row at a time rather than being able to race past images they do not want. Since we specify the thumbnails as the smallest size, I chose to load and cache the android.graphics.Bitmap for the thumbnails so they are ready to display when required.
Just to Review
We’re doing this to provide a customised image gallery. The Android gallery is fully functional and should suit most needs, but if for example you also needed to display the file name or size, date or other information, this solution allows a custom View on the GridView.
Everything else remains the same, and the updated ImageThumbnailsActivity looks like this:
public class ImageThumbnailsActivity extends Activity { private int count; /** * Caching this is a trade off in memory versus performance. * We deliberated use the smallest thumbnails so the size of each should be small (92x92x1 byte = about 10kb each) * but can be significant if there are thousands of images on the device. */ private Bitmap[] windows; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.image_thumbnail); init_phone_image_grid(); } private void init_phone_image_grid() { final String[] columns = { MediaStore.Images.Media._ID }; final String orderBy = MediaStore.Images.Media._ID; Cursor imagecursor = managedQuery(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, null, null, orderBy); int image_column_index = imagecursor.getColumnIndex(MediaStore.Images.Media._ID); this.count = imagecursor.getCount(); this.windows = new Bitmap[this.count]; for(int i=0;i<this.count;i++) { imagecursor.moveToPosition(i); int id = imagecursor.getInt(image_column_index); windows[i] = MediaStore.Images.Thumbnails.getThumbnail(getApplicationContext().getContentResolver(), id, MediaStore.Images.Thumbnails.MICRO_KIND, null); } GridView imagegrid = (GridView) findViewById(R.id.PhoneImageGrid); imagegrid.setAdapter(new ImageAdapter(getApplicationContext())); imagegrid.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(@SuppressWarnings("rawtypes") AdapterView parent, View v, int position, long id) { String[] columns = { MediaStore.Images.Media.DATA, MediaStore.Images.Media._ID }; Cursor actualimagecursor = managedQuery(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, null, null, null); final int dataColumnIndex = actualimagecursor.getColumnIndex(MediaStore.Images.Media.DATA); final int idColumnIndex = actualimagecursor.getColumnIndex(MediaStore.Images.Media._ID); actualimagecursor.moveToPosition(position); final String filename = actualimagecursor.getString(dataColumnIndex); final long imageId = actualimagecursor.getLong(idColumnIndex); final Intent intent = new Intent(ImageThumbnailsActivity.this, ViewImage.class); intent.putExtra("filename", filename); intent.putExtra("dataUid", imageId); actualimagecursor.close(); startActivity(intent); } }); imagecursor.close(); } public class ImageAdapter extends BaseAdapter { private Context mContext; public ImageAdapter(Context c) { mContext = c; } @Override public int getCount() { return count; } @Override public Object getItem(int position) { return position; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ImageView i = (ImageView)convertView; if(i!=null) { i.setImageBitmap(windows[position]); } else { i = new ImageView(mContext.getApplicationContext()); i.setImageBitmap(windows[position]); i.setLayoutParams(new GridView.LayoutParams(92, 92)); } return i; } } }
My application allows the user to capture pictures associated with people and evidence. The images are saved to the SD card and the image URI is saved in a SQLite lookup table (ex: personmedia). I want to display gallery of thumbnails using the URI’s for specific people / things. It seems that the code would be pretty much the same, but I haven’t been able to pull it off. Any help would be very much appreciated.
great stuff, David! Your approach of creating an array of thumbnail bitmaps directly from the existing Media was MUCH better that trying to use the Mediastore.Media.thumbnails. In my case, the media and thumbnails never seemed to be in sync. Openning Gallery sometimes helped, but your approach solved a whole bunch of strange behaviors.
How would I modify this code to display only the images in a specified folder, rather than displaying every image on my device?
good….and very nice…..thanks…