Paolone
Paolone

Reputation: 485

Android stop async loading list element when no more visible

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:

  1. Inform the asynctask about the item of the list it is working for, once loaded image updates it only if the item is visible
  2. When list detaches the view from element (how can I detect this?), stop the asynctask
  3. Override list's OnScrollListener to check when OnScroll event happens if an item exits from visible items' list and the stop its asynctask, if exists.

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

Answers (3)

Paolone
Paolone

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

Karan
Karan

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

Andyccs
Andyccs

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

Related Questions