Reputation: 485
As described in many tutorials and in Android developers pages too, I'm using an async task to load images as thumbnails in a ListView. The task loads full size image from SDcard, resize it and put it in the ImageView of list item's layout.
Everything works well, except for the fact that after scrolling list fast up & down, the image of a single visible element is updated two or three times with different images before getting the right one.
This behavior is related, in my opinion, to the recycling views in ListView: when an asynctask is ready to inject the list's element-X image in the referred view, the view itself might be already been recycled and assigned to list's element-Y.
I'm conscious about some ugliness of my code, for example the fact that I've implemented neither volatile nor persistent cache for thumbnails (targeted for next release), but the problem would be only partially hidden by that.
I found a possible solution using libraries for loading image, but I'm investigating how to fix in my code because the problem is more generally related to using async code in conjunction with list and today I deal with images, but tomorrow I'could face the same problem loading text or any other kind of data.
Possible solutions I'm investigating are:
Is one of these solutions viable or con you suggest a different one?
This is my list's adapter (I'm using an expandable list in a fragment):
@Override
public View getChildView(int groupPosition, final int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
Log.i(TAG, "ExpandableListAdapter.getChildView entered, getting view n. " + groupPosition + "-" + childPosition + ", convertview = " + convertView);
ViewHolder holder;
if (convertView == null) {
convertView = inf.inflate(R.layout.selfie_list_item_layout, parent, false);
holder = new ViewHolder();
holder.date = (TextView) convertView.findViewById(R.id.selfieListItemDateView);
holder.place = (TextView) convertView.findViewById(R.id.selfieListItemPlaceView);
holder.thumb = (ImageView) convertView.findViewById(R.id.selfieListItemThumbView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
Integer mChildIndex = (Integer) getChild(groupPosition, childPosition);
SelfieItem mChildObj = selfies.get(mChildIndex);
String mText = mChildObj.getDate().toString();
holder.date.setText(mText);
holder.thumb.setImageBitmap(BitmapFactory.decodeResource(convertView.getResources(), R.drawable.selfie_place_holder));
File selfieFile = mChildObj.getFile();
new LoadSelfieTask(mFragmentContext).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, selfieFile, holder.thumb);
return convertView;
}
And the following is async code:
@Override
protected Bitmap doInBackground(Object... params) {
File selfieFile = (File)params[0];
Bitmap mySrcBitmap = null;
Bitmap myDestBitmap = null;
if (selfieFile.exists()) {
mySrcBitmap = BitmapFactory.decodeFile(selfieFile.getAbsolutePath());
}
if (mySrcBitmap != null) {
// Get info about view to be updated
mImageViewToBeUpdated = (ImageView) params[1];
mImageHeight = mImageViewToBeUpdated.getHeight();
mImageWidth = mImageViewToBeUpdated.getWidth();
if (mySrcBitmap.getWidth() >= mySrcBitmap.getHeight()){
myDestBitmap = Bitmap.createBitmap(
mySrcBitmap,
mySrcBitmap.getWidth()/2 - mySrcBitmap.getHeight()/2,
0,
mySrcBitmap.getHeight(),
mySrcBitmap.getHeight()
);
}else{
myDestBitmap = Bitmap.createBitmap(
mySrcBitmap,
0,
mySrcBitmap.getHeight()/2 - mySrcBitmap.getWidth()/2,
mySrcBitmap.getWidth(),
mySrcBitmap.getWidth()
);
}
mySrcBitmap = Bitmap.createScaledBitmap(myDestBitmap, mImageWidth, mImageHeight, true);
}
return mySrcBitmap;
}
Thanks in advance for your answers.
Upvotes: 1
Views: 697
Reputation: 485
I found the answer to my question in the example code of this Android Developers Training Lesson.
In ImageWorker.java we can find the method that launches the backgroud task thad loads the image:
/**
* Load an image specified by the data parameter into an ImageView (override
* {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and
* disk cache will be used if an {@link ImageCache} has been added using
* {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCache.ImageCacheParams)}. If the
* image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask}
* will be created to asynchronously load the bitmap.
*
* @param data The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
*/
public void loadImage(Object data, ImageView imageView) {
if (data == null) {
return;
}
BitmapDrawable value = null;
if (mImageCache != null) {
value = mImageCache.getBitmapFromMemCache(String.valueOf(data));
}
if (value != null) {
// Bitmap found in memory cache
imageView.setImageDrawable(value);
} else if (cancelPotentialWork(data, imageView)) {
//BEGIN_INCLUDE(execute_background_task)
final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(mResources, mLoadingBitmap, task);
imageView.setImageDrawable(asyncDrawable);
// NOTE: This uses a custom version of AsyncTask that has been pulled from the
// framework and slightly modified. Refer to the docs at the top of the class
// for more info on what was changed.
task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR);
//END_INCLUDE(execute_background_task)
}
}
A reference to AsyncTask instance is saved in an AsyncDrawable class:
/**
* A custom Drawable that will be attached to the imageView while the work is in progress.
* Contains a reference to the actual worker task, so that it can be stopped if a new binding is
* required, and makes sure that only the last started worker process can bind its result,
* independently of the finish order.
*/
private static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
At the end of background activity, the AsyncTask verifies if it is the last to be "attached" to the view it has to update and performs update only if no other task have been "attached" to the view
/**
* Returns the ImageView associated with this task as long as the ImageView's task still
* points to this task as well. Returns null otherwise.
*/
private ImageView getAttachedImageView() {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask) {
return imageView;
}
return null;
}
/**
* Once the image is processed, associates it to the imageView
*/
@Override
protected void onPostExecute(BitmapDrawable value) {
//BEGIN_INCLUDE(complete_background_work)
// if cancel was called on this task or the "exit early" flag is set then we're done
if (isCancelled() || mExitTasksEarly) {
value = null;
}
final ImageView imageView = getAttachedImageView();
if (value != null && imageView != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "onPostExecute - setting bitmap");
}
setImageDrawable(imageView, value);
}
//END_INCLUDE(complete_background_work)
}
Upvotes: 1
Reputation: 2130
Cancelling AsyncTasks? Is that a good idea? I have found that it does not work many times and postExecute() is always called so its a possibility your image will be still laid out in the listview, maybe a wrong one which will further mess up your scenario..
Upvotes: 0
Reputation: 535
If you need something quick, try Picasso http://square.github.io/picasso/
Your code is, for each row, create an AsyncTask to fetch image from external storage. You will create another AsynTask to fetch the same image again when you scroll back to a row item. I would suggest you to create a cache to store the result of AsynTask, and have proper cache replacement policy.
Upvotes: 1