NOTE: this post is deprecated, please use the RecyclerView for creating lists
I've been playing around with loading some (quite large) images stored in the drawable
folder into a ListView
and in this post I'd like to share the result I came to. Maybe (I hope so) this will save someone plenty of time. I've tested the code I'm posting on several Android 4+ devices and I can say that it runs pretty smoothly and the amount of RAM used stays relatively low. Some explanations go as following:
- we are extending the
BaseAdapter
- images will be loaded in background using an
AsyncTask
- as common for this kind of adapters, we'll be using an
ArrayList<>
parametrized with Objects
of some custom class. In my app, this class is called Weapon
- we will scale the images depending on the screen size
- we will apply a font to the
TextView
in each List Row
Feel free to use this code for any purposes and modify it in any way. The only thing I'm asking for is to test the code properly before claiming that something doesn't work. It works, believe me.
If you have noticed any copy-paste-edit mistakes (since I removed some code that is irrelevant for this little tutorial), your feedback is welcome.
Before I post the code, here's a small state diagram demonstrating the logic of the getView()
method:
The code for the Adapter
class goes below, I've tried to explain everything you need in comments:
public class WeaponAdapter extends BaseAdapter implements View.OnClickListener {
private ArrayList<Weapon> items;
private LayoutInflater inflater = null;
private WeaponHolder weaponHolder;
private Weapon wp;
private Context c;
private Bitmap bmp;
/*--- a simple View Holder class ---*/
static class WeaponHolder {
public TextView text;
public ImageView image, addFav;
public AsyncImageSetter mImageLoader;
}
/*--- Context and all weapons of specified class are passed here ---*/
public WeaponAdapter(ArrayList<Weapon> items, Context c) {
this.items = (ArrayList<Weapon>) items;
inflater = LayoutInflater.from(c);
this.c = c;
}
@Override
public int getCount() {
return items.size();
}
@Override
public Weapon getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
/*--- initialize our Weapon Object ---*/
wp = items.get(position);
if (convertView == null) {
/*--- no View is available. Inflate our list item layout and init the Views we need ---*/
convertView = inflater.inflate(R.layout.category_row, null);
weaponHolder = new WeaponHolder();
weaponHolder.text = (TextView) convertView
.findViewById(R.id.tvCatText);
weaponHolder.image = (ImageView) convertView
.findViewById(R.id.imgCatImage);
weaponHolder.addFav = (ImageView) convertView
.findViewById(R.id.imgAddFav);
convertView.setTag(weaponHolder);
} else {
weaponHolder = (WeaponHolder) convertView.getTag();
/*--- if convertView is not null, cancel the current loading operation to
* improve performance and decrease RAM usage ---*/
weaponHolder.mImageLoader.cancel();
}
/*--- load the image in background ---*/
weaponHolder.mImageLoader = new AsyncImageSetter(c, weaponHolder.image,
wp.getImage(), bmp, weaponHolder.text);
weaponHolder.mImageLoader.execute();
weaponHolder.text.setText(wp.getName());
weaponHolder.addFav.setOnClickListener(this);
return convertView;
}
@Override
public void onClick(View v) {
// do any stuff here
}
}
Here's our AsyncTask
that will load and set the images in background.
NOTE: my Weapon class has a getImage()
method which returns the resId of the drawable
corresponding to a Weapon Object
. You can modify this part in a way it works for you.
public class AsyncImageSetter extends AsyncTask<Void, Void, Bitmap> {
private ImageView img;
private int image_resId;
private Bitmap bmp;
private Context c;
private boolean cancel = false;
private int sampleSize;
private TextView txtGunName;
private Typeface font;
public AsyncImageSetter(Context c, ImageView img, int image_ResId,
Bitmap bmp, TextView txtGunName) {
this.img = img;
this.image_resId = image_ResId;
this.bmp = bmp;
this.c = c;
this.txtGunName = txtGunName;
}
public void cancel() {
cancel = true;
}
@Override
protected void onPreExecute() {
/*--- we hide the Views from the user until the content is ready. This will prevent
* the user from seeing an image being "transformed" into the next one (as a result of
* View recycling) on slow devices.
*/
img.setVisibility(View.GONE);
txtGunName.setVisibility(View.GONE);
font = Typeface.createFromAsset(c.getAssets(), "b_reg.otf");
super.onPreExecute();
}
@Override
protected Bitmap doInBackground(Void... params) {
if (!cancel) {
try {
return decodeAndScale(bmp);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
@Override
protected void onPostExecute(Bitmap result) {
img.setVisibility(View.VISIBLE);
try {
img.setImageBitmap(result);
} catch (Exception e) {
/*--- show an error icon in case something went wrong ---*/
img.setImageResource(R.drawable.ic_warn);
}
txtGunName.setVisibility(View.VISIBLE);
txtGunName.setTypeface(font);
super.onPostExecute(result);
}
private Bitmap decodeAndScale(Bitmap bmp) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = setSampleSize();
return BitmapFactory.decodeResource(c.getResources(), image_resId,
options);
}
private int setSampleSize() {
// TODO add multiple screens check
/*--- modify this method to match your needs ---*/
if (GetSettings.getScreenWidth((Activity) c) >= 320) {
/*--- physical width >= 480px ---*/
sampleSize = 2;
}
return sampleSize;
}}
You may have noticed that I use the getScreenWidth()
method from the GetSettings
class. Its code is quite simple and returns a dp value representing the device's screen width:
public static int getScreenWidth(Activity a) {
Display display = a.getWindowManager().getDefaultDisplay();
DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);
float density = a.getResources().getDisplayMetrics().density;
float dpWidth = outMetrics.widthPixels / density;
return (int) dpWidth;
}
Well, that's all and I hope this post did help someone. Cheers.
P.S. if you are definitely sure something doesn't work, most likely it was caused by your internal app structure that is different from the one I use. In this case, I recommend you to do following steps:
- Ask a new question so you'll be able to add properly formatted code and LogCat output
- Notify me by adding a comment to my post. I will be glad to help you figure out what's wrong