This is just something I put together when trying to figure out the best way to display my images to the user. One thing I did find out is the Android Gallery View sucks! I won’t be using it again for further projects but it does the job at the moment.
The issue with the android Gallery View is it doesn’t recycle it’s views. This means it is very heavy on garbage collection and when you create a custom view you can’t use the nice convertView object like you can with ListViews and others.
There is alternatives to this: EcoGallery However it’s pretty cumbersome, using reflection and you have to override a few classes. So as long as you haven’t got 50+ images in your Gallery I’d say your fine as it is. You could also merge the EcoGallery and this InfiniteGallery if you wish.
Therefore I have written this tutorial to show you how you can create a custom gallery that will allow you to swipe through your images in a continuous loop. Once you get to the end it will just start back at the beginning again without you even noticing.
I have written two implementations, one that will use images from your /res/drawable folder and one that will use your applications SD card cache. This tutorial has a minimum SDK of Android 2.2, you can move this down to 1.6 if you wish (just have to remove the FileManager class). There were two reasons for using 2.2, the long story is a whole new blog post, the short story is 1.6 is dead and SD card caching is 300% easier in >2.2.
As with all my tutorials the code is heavily commented to make it self explanatory, the source code is at the bottom of the post, so here we go:
Basic steps:
- Create a custom Gallery that extends Gallery
- Create a custom adapter to use in that gallery
- Add your custom gallery to your activitys XML file
- Get a reference to your custom gallery in your activity and pass it your images
Here is the custom gallery:
InfiniteGallery.java
package com.blundell.tut.ui.view; import android.content.Context; import android.util.AttributeSet; import android.widget.Gallery; import com.blundell.tut.ui.adapter.InfiniteGalleryCachedAdapter; import com.blundell.tut.ui.adapter.InfiniteGalleryResourceAdapter; /** * This is the custom gallery view * To reference this from XML you need the package name followed by the class name * <i>ex: <com.blundell.tut.ui.view.InfiniteGallery/> </i> * * @author Blundell */ public class InfiniteGallery extends Gallery { public InfiniteGallery(Context context) { super(context); init(); } public InfiniteGallery(Context context, AttributeSet attrs) { super(context, attrs); init(); } public InfiniteGallery(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init(){ // These are just to make it look pretty setSpacing(10); setHorizontalFadingEdgeEnabled(false); } /** * Set the InfiniteGallery to use images cached on the file system * once this method is called the InfiniteGallery will set the adapter and draw it's images * @param images an array of paths <i>ex: /mnt/blundell/cache/ic_launcher.png</i> */ public void setCachedImages(String[] images){ setAdapter(new InfiniteGalleryCachedAdapter(getContext(), images)); setSelection((getCount() / 2)); } /** * Set the InfiniteGallery to use images from your app resources * once this method is called the InfiniteGallery will set the adapter and draw it's images * @param images an array of ids <i>ex: R.drawable.ic_launcher</i> */ public void setResourceImages(int[] images){ setAdapter(new InfiniteGalleryResourceAdapter(getContext(), images)); setSelection((getCount() / 2)); } }
Here is the first adapter. At this point I am just giving you the raw files. If you take the time to download the source files, you may notice the organised package structure. This is taken from google open source examples and is best practice to keep an organised and clean flow to your source files, I highly recommend you follow it.
The first adapter uses images you place in your resources/drawables folder:
InfiniteGalleryResourceAdapter.java
package com.blundell.tut.ui.adapter; import android.content.Context; import android.graphics.drawable.BitmapDrawable; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.Gallery; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import com.blundell.tut.util.AndroidUtils; /** * This is our custom adapter for the Gallery * it will take a list of resource ids to use to draw it's children * it will allow the gallery to be scrolled in a continuous loop * @author Blundell */ public class InfiniteGalleryResourceAdapter extends BaseAdapter { /** The width of each child image */ private static final int G_ITEM_WIDTH = 120; /** The height of each child image */ private static final int G_ITEM_HEIGHT = 80; /** The context your gallery is running in (usually the activity) */ private Context mContext; private int imageWidth; private int imageHeight; /** The array of resource ids to draw */ private final int[] imageIds; public InfiniteGalleryResourceAdapter(Context c, int[] imageIds) { this.mContext = c; this.imageIds = imageIds; } /** * The count of how many items are in this Adapter * This will return the max number as we want it to scroll as much as possible */ @Override public int getCount() { return Integer.MAX_VALUE; } @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) { // convertView is always null in android.widget.Gallery ImageView i = getImageView(); try { // first we calculate the item position in your list, because we have said the adapters size is Integer.MAX_VALUE // the position getView gives us is not use-able in its current form, we have to use the modulus operator // to work out what number in our 'array of paths' this actually equals int itemPos = (position % imageIds.length); i.setImageResource(imageIds[itemPos]); ((BitmapDrawable) i.getDrawable()).setAntiAlias(true); // Make sure we set anti-aliasing otherwise we get jaggies (non-smooth lines) } catch (OutOfMemoryError e) { // a 'just in case' scenario Log.e("InfiniteGalleryResourceAdapter", "Out of memory creating imageview. Using empty view.", e); } return i; } /** * Retrieve an ImageView to be used with the Gallery * @return an ImageView with width and height set to DIP values */ private ImageView getImageView() { setImageDimensions(); ImageView i = new ImageView(mContext); i.setLayoutParams(new Gallery.LayoutParams(imageWidth, imageHeight)); i.setScaleType(ScaleType.CENTER_INSIDE); return i; } /** * Sets the dimensions for each View that is used in the gallery * lazily initialized so that we don't have to keep converting over and over */ private void setImageDimensions() { if (imageWidth == 0 || imageHeight == 0) { imageWidth = AndroidUtils.convertToPix(mContext, G_ITEM_WIDTH); imageHeight = AndroidUtils.convertToPix(mContext, G_ITEM_HEIGHT); } } }
Here is the second adapter, this takes file paths so that you can show images that have been saved to the file system. For example “/mnt/dcim/blundell/lol.png”. It also caches these images so that your not constantly creating new drawables. This is used because as I explained at the top GalleryView doesn’t make use of convertView and so each view has to be recreated each time.
InfiniteGalleryCachedAdapter.java
package com.blundell.tut.ui.adapter; import android.content.Context; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.Gallery; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import com.blundell.tut.util.AndroidUtils; /** * This is our custom adapter for the Gallery * it will take a list of file paths to use to draw it's children * it will allow the gallery to be scrolled in a continuous loop * @author Blundell */ public class InfiniteGalleryCachedAdapter extends BaseAdapter { /** The width of each child image */ private static final int G_ITEM_WIDTH = 120; /** The height of each child image */ private static final int G_ITEM_HEIGHT = 80; /** The context your gallery is running in (usually the activity) */ private Context mContext; private int imageWidth; private int imageHeight; /** The array of file paths to draw */ private final String[] imagePaths; /** A crude cache of the drawables we have retrieved from the file system */ private Drawable[] images; public InfiniteGalleryCachedAdapter(Context c, String[] imagePaths) { this.mContext = c; this.imagePaths = imagePaths; this.images = new Drawable[imagePaths.length]; } /** * The count of how many items are in this Adapter * This will return the max number as we want it to scroll as much as possible */ @Override public int getCount() { return Integer.MAX_VALUE; } @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) { // convertView is always null in android.widget.Gallery // If we don't have an image path - return an empty view if(imagePaths.length == 0){ return getImageView(); } // first we calculate the item position in your list, because we have said the adapters size is Integer.MAX_VALUE // the position getView gives us is not use-able in its current form, we have to use the modulus operator // to work out what number in our 'array of paths' this actually equals // Once we have retrieved the Bitmap with decodeFile we will store it in a lazily initialized array // This means that each image will only be retrieved once from the file system and cached locally within the adapter // next time getView is called with a position that is in the images array it will quickly reference it int itemPos; try { itemPos = (position % imagePaths.length); if(images[itemPos] == null){ String path = imagePaths[itemPos]; BitmapDrawable drawable = new BitmapDrawable(BitmapFactory.decodeFile(path)); drawable.setAntiAlias(true); // Make sure we set anti-aliasing otherwise we get jaggies (non-smooth lines) images[itemPos] = drawable; } } catch (OutOfMemoryError e) { // a 'just in case' scenario Log.e("InfiniteGalleryCachedAdapter", "Out of memory decoding image. Using empty view.", e); return getImageView(); } ImageView i = getImageView(); i.setImageDrawable(images[itemPos]); return i; } /** * Retrieve an ImageView to be used with the Gallery * @return an ImageView with width and height set to DIP values */ private ImageView getImageView() { setImageDimensions(); ImageView i = new ImageView(mContext); i.setLayoutParams(new Gallery.LayoutParams(imageWidth, imageHeight)); i.setScaleType(ScaleType.CENTER_INSIDE); return i; } /** * Sets the dimensions for each View that is used in the gallery * lazily initialized so that we don't have to keep converting over and over */ private void setImageDimensions() { if(imageWidth == 0 || imageHeight == 0){ imageWidth = AndroidUtils.convertToPix(mContext, G_ITEM_WIDTH); imageHeight = AndroidUtils.convertToPix(mContext, G_ITEM_HEIGHT); } } }
Now you have a custom gallery view with an adapter to ‘adapt’ your list of objects, you need to add it to your view hierarchy, i.e. add it to the activity layout. Here I have added two custom InfiniteGallery view’s so that I can show you both ways of loading the adapters (from resources / from file system) :
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingBottom="15dip" android:text="@string/hello" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingBottom="5dip" android:text="Gallery One - from /res/" /> <com.blundell.tut.ui.view.InfiniteGallery android:id="@+id/galleryOne" android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingBottom="15dip" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingBottom="5dip" android:text="Gallery Two - from /sdcard/" /> <com.blundell.tut.ui.view.InfiniteGallery android:id="@+id/galleryTwo" android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingBottom="15dip" /> </LinearLayout>
Once you have added the InfiniteGallery to your activity layout, you can reference it and set the array list of images you want to show. Here we instantiate both Gallerys with two different arrays. Don’t worry how the second list of files on the system is created, just know that it is referencing images on your SD card. If you do want to see how that works check out the FileCacheManager file in the source linked at the bottom of this post.
InfiniteScrollingGalleryActivity.java
package com.blundell.tut.ui.activity; import android.app.Activity; import android.os.Bundle; import com.blundell.tut.R; import com.blundell.tut.ui.view.InfiniteGallery; import com.blundell.tut.util.FileCacheManager; /** * A simple activity that holds two custom Gallery views, these are our custom * InfiniteGallery views that scroll infinitely when you swipe * * @author Blundell */ public class InfiniteScrollingGalleryActivity extends Activity { /** An array of resource drawables to use in the gallery */ int[] resourceImages = { R.drawable.ic_launcher, R.drawable.ic_launcher, R.drawable.ic_launcher, R.drawable.ic_launcher, R.drawable.ic_launcher, R.drawable.ic_launcher }; /** An array of path's to drawables we have cached to our SD card */ String[] cachedOnSDImages = new String[6]; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Set the layout to use setContentView(R.layout.activity_main); // This is just a helper method to initilise some images onto the SD card // in your real application this is up to you saveSomeFilesToSDCard(); // Get a handle to the first gallery in our XML layout and // set the images to draw, this example draws images from /res/drawable InfiniteGallery galleryOne = (InfiniteGallery) findViewById(R.id.galleryOne); galleryOne.setResourceImages(resourceImages); // Get a handle to the second gallery in our XML layout and // set the images to draw, this example draws images from a list of image paths // that in this example are on the SD card in your applications private cache folder InfiniteGallery galleryTwo = (InfiniteGallery) findViewById(R.id.galleryTwo); galleryTwo.setCachedImages(cachedOnSDImages); } /** * You can just ignore this for the TUT, it's just a helper method to place * some images onto the SD card an retrieve their path */ private void saveSomeFilesToSDCard() { FileCacheManager manager = new FileCacheManager(this); String path = manager.saveImage(R.drawable.ic_launcher); for (int i = 0; i < cachedOnSDImages.length; i++) { cachedOnSDImages[i] = path; } } }
Finally to make use of the SD card caching we need to add the SD card permission into the manifest:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="https://schemas.android.com/apk/res/android" package="com.blundell.tut" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".ui.activity.InfiniteScrollingGalleryActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Here is the whole project source files, hope you enjoyed it and please comment!
How would you go about making your “Infinite Scrolling Gallery View” project auto-scroll capable? I like how it cycles through the images continuously, but do not want user to scroll them. I’m assuming a timer of some sort would be needed but am unsure of things beyond that. Thanks.
Cannot find “AndroidUtils” you imported in InfiniteGalleryCachedAdapter.java, could you post the file “AndroidUtils” please?
hey it was in the zip source code at the end 🙂 https://pastebin.com/gbTSZK4H
oh so shame i cant get fastscrolling, its anyway to asynload images from the web or the sdcard?