Async image loading, check if an image is recycled
Asked Answered
R

4

8

This question came to me after reading this: Performance tips (specifically the part named "Async loading"). Basically he's trying to save info about a row to see if it's been recycled yet and only set the downloaded image if the row is still visible. This is how he saves the position:

holder.position = position;

new ThumbnailTask(position, holder)
        .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);

Where ThumbnailTask constructor is:

public ThumbnailTask(int position, ViewHolder holder) {
    mPosition = position;
    mHolder = holder;
}

In onPostExecute() he then does the before mentioned check:

    if (mHolder.position == mPosition) {
        mHolder.thumbnail.setImageBitmap(bitmap);
    }

I just don't see how this gives any result. The Holder and the position are set in the constructor at the same time, to the same value (the position in the holder is the same as the position in mPosition). They don't get changed during the AsyncTask (it's true that the position might change in getView(), but the ones stored in the AsyncTask as private members are never manipulated). What am I missing here?

Also saving the position doesn't seem like a good option in the first place: I believe that it's not guaranteed to be unique, and if I recall correctly it resets itself to 0 after scrolling. Am I thinking in the right direction?

Randolf answered 8/8, 2013 at 20:25 Comment(0)
S
8

Background (you probably know this, but just in case): An adapter contains a collection of objects and uses info from these objects to populate Views (each view is a line item in the list). The list view is in charge of displaying those views. For performance reasons the ListView will recycle views that are no longer visible because they scrolled off the top or the bottom of the list. Here's how it does it:

When the ListView needs a new view to display it calls the Adapter's getView with an integer argument "position" to indicate which object in the Adapter's collection it wants to see (position is just a number from 1 to N -1) where N is the count of objects in the adapter.

If it has any views that are no longer visible, it will pass one of them in to the Adapter, too, as "convertView" This says "reuse this old view rather than creating a new one". A big performance win.

The code in the article attaches a ViewHolder object to each view it creates that, among other things, contains the position of the object requested by the ListView. In the article's code, this position is stashed away inside the ViewHolder along with a pointer to the field within the view that will contain the image. The ViewHolder is attached to the View as a tag (a separate topic).

If the view gets recycled to hold a different object (at a different position) then ListView will call Adapter.getView(newPosition, oldView...) The code in the article will store new position into the ViewHolder attached to the oldView. {make sense so far?) and start loading this new image to put into the view.

Now in the article, it is starting an AsyncTask to retrieve data that should go into the view) This task has the position (from the getView call) and the holder (from the oldView). The position tells it what data was requested. The holder tells it what data should currently be diplayed in this view and where to put it once it shows up.

If the view gets recycled again while the AsyncTask is still running, the position in the holder will have been changed so these numbers won't match and the AsyncTask knows it's data is no longer needed.

Does this make it clearer?

Silhouette answered 8/8, 2013 at 21:20 Comment(3)
As far as the uniqueness of position goes, if the collection of objects contained in the Adapter is unchanged, then each position number will refer a particular object in the Adapter. Even if the view scrolled off, then scrolled back and by accident was asked to contain the same position, it would be the same object so the AsyncTask can safely populate the view. If, on the other hand, the collection of data changes there might be issues to address. That, too is a solvable problem but beyond the scope of the article you referenced.Silhouette
I get it now. I forgot that the Holder was actually being changed outside of the AsyncTask. And it kinda makes sense that position doesn't get reset. I'll accept your answer cause you were first even though Marcins answer was simpler.Randolf
In my implementation, I used a uid so @Dale Wilson concerns do not affect it.Juliettajuliette
W
4

When AsyncTask is passed with ViewHolder and position it is given value of position (say 5) and value of reference (not a copy) to ViewHolder object. He also puts current position in ViewHolder (said 5), but the whole "trick" here is that for recycled views, the old ViewHolder object is also re-used (in linked article):

} else {
   holder = convertView.getTag();
}

so whatever code references that particular ViewHolder object, will in fact check against its position member value at the moment of doing check, not at the moment of object creation. So the onPostExecute check makes sense, because position value passed to task constructor remains unchanged (in our case it has value of 5) as it is primitive, but ViewHolder object can change its properties, if view will be reused before we reach onPostExecute.

Please note we do NOT copy ViewHolder object in the constructor of the task, even it it looks so. It's not how Java works :) See this article for clarification.

Also saving the position doesn't seem like a good option: I believe that it's not guaranteed to be unique, and it resets itself to zero after scrolling. Is this true?

No. Position here means index in *source data set, not visible on the screen. So if you got 10 items to display, but your screen fits only 3 at the time, your position will be in range 0-9 and visibility of the rows does not matter.

Wite answered 8/8, 2013 at 21:26 Comment(0)
J
0

As I understand you are trying to cancel the async-loading-task of the image when the view recycled, and no longer on screen.

To achieve that you can set up an RecyclerListener to the listview. It will be invoked when the listview don't need this view (when is not on screen), just before it passes it as a recycled view to the Adapter.

within this listener you can cancel your download task:

theListView.setRecyclerListener(new RecyclerListener() {
    @Override
    public void onMovedToScrapHeap(View view) {
        for( ThumbnailTask task : listOfAllTasks )
           task.viewRecycled(task);
    }
});

and within ThumbnailTask :

public void viewRecycled(View v){
   if(mHolder.theWholeView == v)
      v.cancel();
}

Don't for to implement the cancel.


Note that its not the best approach since you should keep track of all your asynctask tasks. note that you could also cancel the task within the adapter where you also get the

public View getDropDownView (int position, View recycledView, ViewGroup parent){
  //.. your logic
}

but note that this might require you to allocate the ThumbnailTask within the adapter with is not good practice.


note that you could also use image loading libraries that do eveything for you, from async download to chaching. for instance : https://github.com/nostra13/Android-Universal-Image-Loader

Julejulee answered 8/8, 2013 at 21:9 Comment(1)
Thanks for the answer. This is definetly one way to solve the problem and I will look into it. I've also upvoted your lenghty answer, but it's not exactly what I am asking. I never said that I want to solve this problem. I just want to find out if the way that the author of that text was doing it was even valid.Randolf
C
0

The accepted answer and Marcin's post already describe perfectly what's supposed to happen. However, the linked webpage does not and the google site on this topic is also very vague and only a reference for people who already know about the "trick". So here's the missing part, for future references, which shows the necessary additions to getView().

// The adapter's getView method
public View getView(int position, View convertView, ViewGroup parent) {
    // Define View that is going to be returned by Adapter
    View newViewItem;
    ViewHolder holder;

    // Recycle View if possible
    if (convertView == null) {
        // No view recycled, create a new one
        LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        newViewItem = inflater.inflate(R.layout.image_grid_view_item, null);

        // Attach a new viewholder
        holder = new ViewHolder();
        holder.thumbnail = (ImageView) newViewItem.findViewById(R.id.imageGridViewItemThumbnail);
        holder.position = position;
        newViewItem.setTag(holder);
    } else {
        // Modify "recycled" viewHolder
        holder = (ViewHolder) convertView.getTag();
        holder.thumbnail = (ImageView) convertView.findViewById(R.id.imageGridViewItemThumbnail);
        holder.position = position;

        // Re-use convertView
        newViewItem = convertView;
    }
    // Execute AsyncTask for image operation (load, decode, whatever)
    new LoadThumbnailTask(position, holder).execute();

    // Return the ImageView
    return newViewItem;
}

// ViewHolder class, can be implemented inside adapter class
static class ViewHolder {
    ImageView thumbnail;
    int position;
}
Casias answered 6/3, 2015 at 17:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.