I have a ListView with thumbnail images. All visible rows in the ListView don't have problem.
But for those new rows below the visible ones, even though I tried not to assign any images to those thumbnail ImageViews, images started from the first row are copied exactly the same order as in visible rows. I set breakpoints at those lines of codes assigning the images at thumbnail ImageViews, no breakpoints are hit but still get the images. What is the theory behind? And how can I stop assigning images automatically at the rows below the visible ones.
Thanks
EDIT1:
public View getView(int position, View convertView, ViewGroup parent) {
View vi=convertView;
ViewHolder viewHolder=new ViewHolder();
LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if(vi==null){
vi = inflater.inflate(R.layout.list_row, parent, false);
viewHolder.id=(TextView)vi.findViewById(R.id.title);
viewHolder.thumbnailImage=(ImageView)vi.findViewById(R.id.list_image);
viewHolder.activationStatus = (TextView)vi.findViewById(R.id.activated);
//lazy load image
BitmapWorkerTask task = new BitmapWorkerTask(viewHolder.thumbnailImage);
//if beyond visible rows, position
//becomes zero again, at that time cnt is not zero
//so task is not executed, to prevent image assignment
//for rows below the visible ones
if(position == cnt){
String id = listIDs.get(position);
task.execute(id);
cnt++;
}else{
cnt = 0;
}
//Lazy image update
class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
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(String... params) {
Bitmap bitmap = null;
dbHelper.open();
byte[] img_bytes = dbHelper.getImagebyIDnumber(params[0]);
bitmap = BitmapFactory.decodeByteArray(img_bytes, 0, img_bytes.length);
dbHelper.close();
return bitmap;
}
// 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);
}
}
}
}
Check your Layout. Perhaps you settet android:src = "#drawable..." by default for your images.
Configure your listView data in your adaper.
EDITED 22.08.
try to use this:
if(vi==null){
...
//findView by ID here;
...
vi.setTag(viewHolder);
} else {
viewHolder = ( ViewHolder ) vi.getTag();
}
//to do what you want here;
//setting values etc.
You can find good explanation 'how to work with ViewHolder' here ViewHolder Pattern
This is because ListView reuses views getting invisible for visible views. If you want to hide images, you need to do this explicitly in getView() method of your adapter.
Related
I have a listview which loads images in every cell in async. When I try to scroll down slowly(after all the images in the current view are loaded), it works flawlessly.
But when I try to scroll down before they are even loaded and scroll up, I face this issue. The cells start to show up images which don't correspond to them.
My getView method looks like this:
public View getView(final int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
View rowView = null;
if(convertView == null) {
rowView = inflater.inflate(R.layout.list_posts_item, null);
final Holder holder=new Holder();
holder.tvTitle=(TextView) rowView.findViewById(R.id.tvTitleNamePost);
holder.ivPrimaryImage=(ImageView) rowView.findViewById(R.id.ivPrimaryImage);
holder.tvLocality=(TextView) rowView.findViewById(R.id.tvLocalityPosts);
holder.tvDateCreated=(TextView) rowView.findViewById(R.id.tvDateCreated);
rowView.setTag(holder);
}else {
rowView=convertView;
}
Holder holder = (Holder)rowView.getTag();
holder.ivPrimaryImage.setId(position);
holder.ivPrimaryImage.setTag(listOfPosts.get(position).getPostId());
holder.ivPrimaryImage.setImageBitmap(null); // Added for flickering issue
holder.tvTitle.setText(listOfPosts.get(position).getTitle());
holder.tvLocality.setText(listOfPosts.get(position).getLocality());
holder.tvDateCreated.setText(listOfPosts.get(position).getCreatedDate());
postId = listOfPosts.get(position).getPostId();
Image image = new Image();
image.setImg(holder.ivPrimaryImage);
if (!"N".equalsIgnoreCase(listOfPosts.get(position).getHasImage()) ) {
if(!tagsCaching.containsKey(postId))
new GetPrimaryImages().execute(image);
else
holder.ivPrimaryImage.setImageBitmap(tagsCaching.get(postId));
}
return rowView;
}
And my Async call class looks like this:
public class GetPrimaryImages extends AsyncTask<Image, Void, Bitmap> {
ImageView imageView = null;
protected Bitmap doInBackground(Image... images) {
this.imageView=images[0].getImg();
// Building Parameters
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("postid",(String)(this.imageView.getTag()) ));
json = jsonParser.makeHttpRequest(CommonResources.getURL("get_primary_image"),
"POST", params);
if(json == null){
return null;
}
Log.d("Fetching Image",imageView.getTag()+ json.toString());
tagsDownloaded.add((String)imageView.getTag());
// check for success tag
String TAG_SUCCESS = "success";
try {
int success = json.getInt(TAG_SUCCESS);
if (success == 0) {
image = json.getString("primaryimage");
}
} catch (JSONException e) {
e.printStackTrace();
}
return getImage(image);
}
/**
* After completing background task Dismiss the progress dialog
* **/
protected void onPostExecute(Bitmap result) {
tagsCaching.put((String)imageView.getTag(), result);
imageView.setImageBitmap(result);
}
public Bitmap getImage(String imageString) {
if("null".equalsIgnoreCase(imageString)){
return null;
}else{
byte[] decodedString = Base64.decode(imageString, Base64.DEFAULT);
Bitmap decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
//image.setImageBitmap(decodedByte);
return decodedByte;
}
}
}
Edit:
I added a new instance variable to Holder:
public class Holder
{
TextView tvTitle;
ImageView ivPrimaryImage;
TextView tvLocality;
TextView tvDateCreated;
int position;
}
Set the same in the getView:
holder.position = position;
And passed the holder object to the Async task:
new GetPrimaryImages(position, holder).execute(image);
And modified the Async call class as follows:
1. Added cancel to the http call
2. Changed the onPostExecute method
public class GetPrimaryImages extends AsyncTask<Image, Void, Bitmap> {
int mPosition;
Holder mHolder;
public GetPrimaryImages(int position, Holder holder){
mPosition = position;
mHolder = holder;
}
ImageView imageView = null;
protected Bitmap doInBackground(Image... images) {
this.imageView=images[0].getImg();
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("postid",(String)(this.imageView.getTag()) ));
JSONObject json;
if(mHolder.position == mPosition)
json = jsonParser.makeHttpRequest(CommonResources.getURL("get_primary_image"),
"POST", params);
else {
json = null;
cancel(true);
}
// check log cat fro response
if(json == null){
return null;
}
Log.d("Fetching Image",imageView.getTag()+ json.toString());
tagsDownloaded.add((String)imageView.getTag());
// check for success tag
String TAG_SUCCESS = "success";
try {
int success = json.getInt(TAG_SUCCESS);
if (success == 0) {
image = json.getString("primaryimage");
}
} catch (JSONException e) {
e.printStackTrace();
}
return getImage(image);
}
protected void onPostExecute(Bitmap result) {
if (mHolder.position == mPosition) {
tagsCaching.put((String) imageView.getTag(), result);
imageView.setImageBitmap(result);
}
}
public Bitmap getImage(String imageString) {
//needs to wait
if("null".equalsIgnoreCase(imageString)){
return null;
}else{
byte[] decodedString = Base64.decode(imageString, Base64.DEFAULT);
Bitmap decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
//image.setImageBitmap(decodedByte);
return decodedByte;
}
}
}
It seems to be working. :)
Now my doubt is what would be the best way to cache the images? Should be writing it to a file? and reading it from it every time I scroll up?
The problem is, while your async task ends its background operation, the element it was linked to has been recycled to hold another element of your collection.
Let's focus on elements position, and let's say your listview can display up to 4 elements.
The first time the listview calls getview for the first 4 elements, and four asynctasks are created and run.
Then you scroll to shouw positions 11 - 15, and the first element (the one related to position 1) gets recycled for position 11 before the asynctask ends.
Then the asynctask ends, and what you see is the image related to post 11 with the bitmap related to post 1.
A way to avoid this is knowing in the asynctask that the view was recycled, as suggested in this old post from Lucas Rocha.
Performance tips with listview
Check the post for insights on how listview works too:
Main "problem" is with ListViews implementation of reusing views and serial providing of AsyncTasks.
1) In ListView's adapter you correctly implement reusing of items. In ListView there are rendered only few items (items visible on screen + few top and down). If you start scrolling items which went out of screen are destroyed and theirs views are passed asi parameter to public View getView(final int position, View convertView, ViewGroup parent) as convertView.
This is first problem. List is reusing items.
2) Second thing is that AsyncTask is performed on another thread but more instances of asyncTask are performed on the same thread. it means they are performed serially. If you scroll quickly enough then you can make more requests on loading images with AsyncTask. At one time, there can be only few (i think 5) AsyncTask's jobs waiting in queue. Another ariving is ignored. So if you swipe quick enough you get this queue of 5 full and another image requests are ignored.
And this is It. You scroll, you start loading few images and the new displayed images are ignored. When you stop after while all AsyncTasks end and you got some "random" (previosly displaying) image loaded in your list item.
Sollution
This thing was discussed manny times and is solved. It is enough to use for example Picaso library:
https://futurestud.io/blog/picasso-adapter-use-for-listview-gridview-etc/
To add the answers, I would implement an image cache (e.g. as an URL-to-WeakReference-to-image hashmap). The getView() would access that cache and, if the image is not there, leave a request. When the image is loaded, the cache would examine the request list and notify the views that posted the requests (passing them both URL and the image). The views would compare the URL passed in notification to their current URL and either use the image or ignore it (if the view went out of screen or was reused, the image must be ignored).
Why request list. It is possible that multiple views manage to request some image and get reused before the image is loaded (especially if you scroll the list up and down several times).
My ListView jumps/freezes while scrolling up ( Scrolling down has no problem ) when I add images in, this doesn't happen when my keyboard is up.
I don't own the DownloadImageWithURLTask class, I cannot remember where I got it from.
public class chatAdapter extends ArrayAdapter<chatModel> {
private Context context;
public String userName = null;
public ImageView hold = null;
private static class ViewHolder{
TextView userName;
TextView userMessage;
ImageView userImage;
}
public chatAdapter(Context c, List<chatModel> items){
super(c, 0, items);
this.context = c;
}
class DownloadImageWithURLTask extends AsyncTask<String, Void, Bitmap> {
ImageView bmImage;
public DownloadImageWithURLTask(ImageView bmImage) {
this.bmImage = bmImage;
}
protected Bitmap doInBackground(String... urls) {
String pathToFile = urls[0];
Bitmap bitmap = null;
try {
InputStream in = new java.net.URL(pathToFile).openStream();
bitmap = BitmapFactory.decodeStream(in);
} catch (Exception e) {
Log.e("Error", e.getMessage());
e.printStackTrace();
}
return bitmap;
}
protected void onPostExecute(Bitmap result) {
bmImage.setImageBitmap(result);
}
}
#Override
public View getView(int position, View convertView, ViewGroup parent){
final chatModel chatModel = getItem(position);
final ViewHolder viewHolder;
if (convertView == null){
viewHolder = new ViewHolder();
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.activity_chat_box, parent, false);
TextView userName = (TextView)convertView.findViewById(R.id.usern);
TextView userMessage = (TextView)convertView.findViewById(R.id.msg);
final ImageView userImage = (ImageView)convertView.findViewById(R.id.imageView8);
userName.setText(chatModel.userName);
userMessage.setText(chatModel.chatMessage);
final String userAvatarURL = "http://downtowndons.eu/Downtown/Avatar/" + chatModel.userName;
DownloadImageWithURLTask downloadTask = new DownloadImageWithURLTask(userImage);
downloadTask.execute(userAvatarURL);
} else {
viewHolder = (ViewHolder)convertView.getTag();
}
convertView.setAnimation(AnimationUtils.loadAnimation(context, R.anim.abc_slide_in_bottom));
return convertView;
}
}
If your using AsyncTask to load Image in imageView.While scrolling rapidly in listview you may had chance to get OutofMemory exception.In order to solve those type of exceptions you need to handle Memory cache mechanism for handling images.By default there are plenty of image loading libraries available in android.One of the easiest library is Universal Image Loader.
Use Universal image loader for downloading images asynchronously.
http://github.com/nostra13/Android-Universal-Image-Loader
The Library itself has a sample code to download image.you may refer it.. After downloading library add library with your project and insert the below code at necessary place
Write this code in adapter constructor
ImageLoader imageloader = ImageLoader.getInstance();
imageloader.init(ImageLoaderConfiguration.createDefault(context));
DisplayImageOptions options; = new DisplayImageOptions.Builder()
.showImageForEmptyUri(R.drawable.ic_empty)
.showImageOnFail(R.drawable.ic_error)
.resetViewBeforeLoading(true).cacheOnDisk(true)
.imageScaleType(ImageScaleType.EXACTLY)
.bitmapConfig(Bitmap.Config.RGB_565).considerExifParams(true)
.cacheInMemory(true)
.displayer(new FadeInBitmapDisplayer(300)).build();
Replace this code in getView instead of yours code.
if(convertView==null){
// ... other stuff
viewHolder.userName=(TextView)convertView.findViewById(R.id.usern);
viewHolder.userMessage=(TextView)convertView.findViewById(R.id.msg);
viewHolder.userImage= (ImageView)convertView.findViewById(R.id.imageView8);
}else{
viewHolder=(ViewHolder)convertView.getTag();
}
viewHolder.userName.setText(chatModel.userName);
viewHolder.userMessage.setText(chatModel.chatMessage);
final String userAvatarURL="http://downtowndons.eu/Downtown/Avatar/"+chatModel.userName;
imageloader.displayImage(userAvatarURL, viewHolder.userImage);
To fetch images from the net, try using one of these two libraries mentioned in this answer: https://stackoverflow.com/a/22862991/1018109
I'd strongly suggest following all answers and comments on this question: Lazy load of images in ListView
The ListView Freezes because it's trying to Re-Download the images again.
One on the best practices in this case is to cache image into memory using Android LruCache or Disk using DiskLruCache for efficiency..
Check this link out, it will help you a lot..
http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html#disk-cache
Initially, your problem is fetching and loading images on scroll. Also you don't store earlier downloaded images in cache.
To solve your downloading images problem use any image caching library
Universal Image Loader
Lazy List Loader
Volley Image Loader
Glide
Picasso
Go with any of them and your problem will be solved! I have mentioned example of each library with listview. Check it out!
I'm relatively new to this, but I'm lost with your code. It doesn't look like you're actually doing anything with your viewholder pattern. Your viewholder should be a quick way of getting your two textviews and one imageview every time they get updated, which is what speeds up scrolling for listview. However, you created it but didn't actually do anything with it. So the pattern should be something like:
if(convertView==null){
// ... other stuff
viewHolder.userName=(TextView)convertView.findViewById(R.id.usern);
viewHolder.userMessage=(TextView)convertView.findViewById(R.id.msg);
viewHolder.userImage=(ImageView)convertView.findViewById(R.id.imageView8);
}else{
viewHolder=(ViewHolder)convertView.getTag();
}
viewHolder.userName.setText(chatModel.userName);
viewHolder.userMessage.setText(chatModel.chatMessage);
final String userAvatarURL="http://downtowndons.eu/Downtown/Avatar/"+chatModel.userName;
DownloadImageWithURLTask downloadTask=new DownloadImageWithURLTask(viewHolder.userImage);
downloadTask.execute(userAvatarURL);
Being a complete novice to Android and (admittedly) not the strongest programmer - I want to ask for some advice on loading thumbnail images into a Bitmap Array, which is loaded into a custom adapter.
The thumbnails are very small (around 5KB).
I add the thumbnails to a Bitmap array in an Async task. I am using drawables which are dummy images. So I load the entire list with dummy images (I load the actual images later on).
I am worried if the user browses a folder with 200+ images. I could possibly get an out of memory error. I want a way to prevent this, perhaps only load what is needed in the visible display, and load more if needed?
I have read a lot of other questions and advice on recycling Bitmaps, but I'm still not sure where tog o from here.
#Override
protected Boolean doInBackground(DbxFileSystem... params) {
//Opens thumbnails for each image contained in the folder
try {
DbxFileSystem fileSystem = params[0];
Bitmap image=null;
int loopCount=0; //I use this to identify where in the adapter the real image should go
for (DbxFileInfo fileInfo: fileSystem.listFolder(currentPath)) {
try{
if(!fileInfo.isFolder)
{
image = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
pix.add(image);
paths.add(fileInfo.path);
loopCount++;
}
else
{
//must be a folder if it has no thumb, so add folder icon
image = BitmapFactory.decodeResource(getResources(), R.drawable.dbfolder);
pix.add(image);
paths.add(fileInfo.path);
loopCount++;
}
}
catch(Exception e)
{
e.printStackTrace();
}
System.gc();
}
}
catch (Exception e) {
e.printStackTrace();
return false;
} finally {
loadingDialog.dismiss();
}
return true;
}
Here is the getView from the Custom Adapter:
public View getView(final int position, View arg1, ViewGroup arg2) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = arg1;
ViewHolder holder;
if (arg1 == null) {
LayoutInflater vi = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.list_row, null);
holder = new ViewHolder();
holder.title = (TextView) v.findViewById(R.id.filename);
holder.iconImage = (ImageView) v.findViewById(R.id.list_image);
holder.checkbox = (CheckBox)v.findViewById(R.id.checkBox1);
v.setTag(holder);
} else {
holder = (ViewHolder) v.getTag();
}
holder.title.setText(folderName.get(position).toString());
holder.iconImage.setImageBitmap(images.get(position));
The first thing you need to know is that when using an adapter, the views are created only when they are displayed on the screen. It means that you don't need and you must not decode all the bitmaps.
The best practice is to decode each bitmap in an AsyncTask when the associated view is created. The bitmap will be decoded in the doInBackground method and set to the ImageView in the onPostExecute method (as it's executed on the UI thread).
Then you might also want to use RAM or disk cache to reload previously decoded bitmaps more efficiently.
Please take a look at http://developer.android.com/training/displaying-bitmaps/index.html for more information about how to display bitmaps efficiently.
I'm trying to add images in a ListView which has an ArrayAdapter. Fyi, the toList() is a conversion from iterator to a list of the given DBObject.
I override the View getView() and set a textview and an image.
private static class EventAdapter extends ArrayAdapter<DBObject> {
public EventAdapter(Context context, int resource, Iterable<DBObject> events) {
super(context, resource, toList(events));
}
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
LayoutInflater vi = LayoutInflater.from(getContext());
v = vi.inflate(R.layout.adapter_event_list, null);
DBObject event = getItem(position);
if (event != null) {
//Get the logo if any
if( ((DBObject)event.get("events")).containsField("logo") ){
String logoURL = ((DBObject)((DBObject)event.get("events")).get("logo")).get("0").toString();
ImageView eventLogo = (ImageView) v.findViewById(R.id.eventLogoList);
new setLogo().execute(logoURL, eventLogo);
}
TextView title= (TextView) v.findViewById(R.id.eventTitleList);
title.setText( ((DBObject)event.get("events")).get("title").toString() );
}
return v;
}
protected static <T> List<T> toList( Iterable<T> objects ) {
final ArrayList<T> list = new ArrayList<T>();
for( T t : objects ) list.add(t);
return list;
}
//setLogo() method here. See below
}
The text in the textview is fine. However the images are getting messed up. They seem to load in wrong places in the list. The route of the code is: 1)Get from the DB (async) 2)populate the ListView 3) while populating load each image(second async).
Here is the setLogo() AsyncTask which is inside the EventAdapter above:
private class setLogo extends AsyncTask<Object,Void,Bitmap>{
ImageView eventLogo = null;
#Override
protected Bitmap doInBackground(Object...params) {
try{
Bitmap eventImage = downloadBitmap((String) params[0]);
eventLogo = (ImageView) params[1];
return eventImage;
}
catch(Exception e){
e.printStackTrace();
return null;
}
}
#Override
protected void onPostExecute(Bitmap eventImage) {
if(eventImage!=null && eventLogo!=null){
eventLogo.setImageBitmap(eventImage);
}
}
}
I did so (using an Async) which I believe is the correct way to load images from urls. I saw this post on multithreading and from which I borrowed the downloadBitmap() method.
As explained above the images are loaded in wrong places of the ListView. What can be a robust way to load them?
Also the idea to pass the v.findViewById(R.id.eventLogoList) inside the AsyncTask is that the program will distinguish each adapter's ImageView but it seems it doesn't.
Update
After following the problem that is causing this mix I found this SO question.
I altered my code in order to check if the if is causing the problem.
//Get the logo if any
if( ((DBObject)event.get("events")).containsField("logo") ){
String logoURL = ((DBObject)((DBObject)event.get("events")).get("logo")).get("0").toString();
ImageView eventLogo = (ImageView) row.findViewById(R.id.eventLogoList);
//new setLogo().execute(logoURL, eventLogo);
TextView title= (TextView) row.findViewById(R.id.eventTitleList);
title.setText( "Shit happens" );
}
Let's say I have 40 items. The Shit happens is set on the fields that a logo field exists. If I scroll down/up the order changes and the text gets messed up. It is because the stack created inside the loop is small than the maximum of the list..I guess... I am still struggling.
PS: I found this easy library to load images asynchronously instead of DYI stuff.
Update 2
I added an else with a static url. Because of the time it take to the image to load they are still misplaced.
I would really go for a good library like Picasso.
It will handle all the hard part for you and it's very well written.
http://square.github.io/picasso/
I am trying to load images dynamically in a list view in android. The code below loads the image according to the viewed position and loads the image in that array position. However, as it gets the data in background, sometimes the image in the first array is loaded in the second imageview position, second in third etc. I guess when the getDataInBackground part of the first item in the array is already finished the sytem is trying to create the second cell at that time and loads it to the second cell. How can I handle this, I am using android studio AVD with version kitkat and nexus 5 emulator.
private class MyListAdapter extends ArrayAdapter<String> {
public MyListAdapter() {
super(getActivity().getApplicationContext(), R.layout.fragment_users_cell, myItemList);
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View cellView = convertView;
if (cellView == null){
cellView = getActivity().getLayoutInflater().inflate(R.layout.fragment_users_cell, parent, false);
}
cellProfileImage = (ImageView) cellView.findViewById(R.id.fragment_users_cell_profileImg);
System.out.println("The current position" + position);
if (resultsImageFiles.get(position)==null) {
Bitmap image = BitmapFactory.decodeResource(getActivity().getResources(), R.drawable.ph2);
cellProfileImage.setImageBitmap(image);
} else {
ParseFile file = resultsImageFiles.get(position);
file.getDataInBackground(new GetDataCallback() {
#Override
public void done(byte[] data, ParseException e) {
if (e == null) {
Bitmap image = BitmapFactory.decodeByteArray(data, 0, data.length);
cellProfileImage.setImageBitmap(image);
}
}
});
}
return cellView;
}
}
Listview reuse convertview --> Some itemView has same value for view. In this case, you have to remove cellProfileImage before Bitmap loaded.
try to remove all cellProfileImage.setImageBitmap(image); and move it above return cellView; don't forget to declare Bitmap image after View cellView = convertView; but you can also create a class viewHolder that contain your imageview like this link:http://lucasr.org/2012/04/05/performance-tips-for-androids-listview/