15

I have one ListView which can hold an image. It depends if image exists or not in SDCARD.

Here my example code:

public class MainActivity extends Activity  {

    ListView mListView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mListView = new ListView(this);
        setContentView(mListView);

        String[] arr = new String[] { 
                "/example/images/1.jpg", "/example/images/2.jpg",  
                "/example/images/3.jpg", "/example/images/4.jpg",  
                "/example/images/5.jpg", "/example/images/6.jpg", 
                "/example/images/7.jpg", "/example/images/8.jpg",  
                "/example/images/9.jpg", "/example/images/1.jpg", 
                "/example/images/2.jpg", "/example/images/3.jpg",  
                "/example/images/4.jpg", "/example/images/5.jpg",  
                "/example/images/6.jpg", "/example/images/7.jpg",  
                "/example/images/8.jpg", "/example/images/9.jpg", 
                "/example/images/1.jpg", "/example/images/2.jpg",  
                "/example/images/3.jpg", "/example/images/4.jpg",  
                "/example/images/5.jpg", "/example/images/6.jpg", 
                "/example/images/7.jpg", "/example/images/8.jpg",  
                "/example/images/9.jpg", "/example/images/1.jpg", 
                "/example/images/2.jpg", "/example/images/3.jpg",  
                "/example/images/4.jpg", "/example/images/5.jpg",  
                "/example/images/6.jpg", "/example/images/7.jpg",  
                "/example/images/8.jpg", "/example/images/9.jpg"}; 

        List<String> list = Arrays.asList(arr);

        MyAdapter adapter = new MyAdapter(this, R.layout.listitem_imv, list);

        mListView.setAdapter(adapter);
    }

    class MyAdapter extends ArrayAdapter<String>{

        List<String> mList;
        LayoutInflater mInflater;
        int mResource;

        public MyAdapter(Context context, int resource,
                List<String> objects) {
            super(context, resource, objects);

            mResource = resource;
            mInflater = getLayoutInflater();
            mList = objects;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View view;

            if(convertView == null){
                view = mInflater.inflate(mResource, null);
            }else{
                view = convertView;
            }

            ImageView imageView = (ImageView) view.findViewById(R.id.imv);
            TextView textView = (TextView) view.findViewById(R.id.txv);

                            imageView.setTag(mList.get(position));//tag of imageView == path to image
            new LoadImage().execute(imageView);
            textView.setText(mList.get(position).toString());

            return view;
        }       
    }

    class LoadImage extends AsyncTask<Object, Void, Bitmap>{

        private ImageView imv;
        private String path;


        @Override
        protected Bitmap doInBackground(Object... params) {
            imv = (ImageView)   params[0];

            path = imv.getTag().toString();

            Bitmap bitmap = null;
            File file = new File( 
                    Environment.getExternalStorageDirectory().getAbsolutePath() + path);

            if(file.exists()){
                bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
            }

            return bitmap;
        }
        @Override
        protected void onPostExecute(Bitmap result) {
            if(result != null && imv != null){
                imv.setVisibility(View.VISIBLE);
                imv.setImageBitmap(result);
            }else{
                imv.setVisibility(View.GONE);
            }
        }
    }
}

The 'sdcard/example/images' directory has the images: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 6.jpg, 7.jpg and 9.jpg. the expected result is: example

But, if I scroll the list quickly, some images are inserted in the wrong items. It happens due to use of convertView in getView() method.

If I use the following code, the code works fine:

        //if(convertView == null){
        //  view = mInflater.inflate(mResource, null);
        //}else{
        //  view = convertView;
        //}
        view = mInflater.inflate(mResource, null);

When list scrolled quickly, two asyncTasks can reference one same View, due to use of convertView. How Can I cancel AsyncTask when the View is no longer visible?(and is useb by another item of ListView)

edit

            @Override
    protected void onPostExecute(Bitmap result) {
        if(result != null && imv != null){

            if(imv.getTag().equals(path)){
                imv.setVisibility(View.VISIBLE);
                imv.setImageBitmap(result);
            }else{
                imv.setVisibility(View.GONE);
            }

        }else{
            imv.setVisibility(View.GONE);
        }
    }
Rodrigo
  • 5,104
  • 5
  • 37
  • 72

6 Answers6

42

You can send in the ImageView to the task constructor and keep a reference to the image path there. Now at onPostExecute, check if the current tag of the ImageView is the same as the one that you started with. If yes, then set the image. If no, don't do anything.

However, this means that the image will be downloaded in any case. You'll just not set the wrong image on the view.

EDIT: First pass the ImageView to the task constructor:

new LoadImage(imageView).execute()

Then save a reference to the ImageView and image path in LoadImage constructor. It is important to save the path in the constructor and not in doInBackground to ensure that we don't run into multi threading problems. Then at onPostExecute we check the current path.

class LoadImage extends AsyncTask<Object, Void, Bitmap>{

        private ImageView imv;
        private String path;

        public LoadImage(ImageView imv) {
             this.imv = imv;
             this.path = imv.getTag().toString();
        }

    @Override
    protected Bitmap doInBackground(Object... params) {
        Bitmap bitmap = null;
        File file = new File( 
                Environment.getExternalStorageDirectory().getAbsolutePath() + path);

        if(file.exists()){
            bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
        }

        return bitmap;
    }
    @Override
    protected void onPostExecute(Bitmap result) {
        if (!imv.getTag().toString().equals(path)) {
               /* The path is not same. This means that this
                  image view is handled by some other async task. 
                  We don't do anything and return. */
               return;
        }

        if(result != null && imv != null){
            imv.setVisibility(View.VISIBLE);
            imv.setImageBitmap(result);
        }else{
            imv.setVisibility(View.GONE);
        }
    }

}
shabyasachi
  • 660
  • 5
  • 6
  • I tried this. see edit. But images are still being put in wrong items. – Rodrigo Oct 11 '11 at 18:28
  • @Rodrigo Also if you cache the image you download before you check if it is the correct one, it means it's not a wasted download as it can be used by your other tasks – Blundell Aug 19 '12 at 13:15
  • 1
    Yes @Droider, but look at post date. – Rodrigo Feb 24 '13 at 23:58
  • 3
    @shabyasachi this solution is not correct, I do not understand the upvotes. The ListView does not work like this, it uses always the same view items for displaying the content, and when scrolling, it uses recycled views for display the new items. That solution is WRONG AT ALL. – Didac Perez Parera Aug 28 '13 at 18:43
  • 1
    @DídacPérez : Check the big comment in onPostExecute. That comment explains how view recycling is handled. – shabyasachi May 28 '14 at 16:10
  • Got it working.. Thanks, now the listview loaded faster. – TPG Dec 21 '14 at 04:35
  • In case you are using reusable Views, you can take a WeakReference to your ImageView and check the Tag on the ImageView before loading Bitmaps into it. I hope this helps anyone who is using Reusable Views. – Salman Khakwani Oct 19 '16 at 05:34
9

This Android Developers Blog post will give you a complete reference project for this complete with caching. Just replace the Http access code with SD card file reads.

Jeff Axelrod
  • 25,625
  • 29
  • 137
  • 239
2

I hope this helps. After lot of search I have this working solution.

public class CustomAdapter extends ArrayAdapter<String>{
    /*
    public CustomAdapter(Context context , String[] video) {
        super(context,R.layout.custom_row, video);
    }
*/

private final Activity context;
private final String[] video;

static class ViewHolder {
    public TextView videoTitle;
    public ImageView videoThumbnail;
    public int position;
    public String path;
}

public CustomAdapter(Activity context, String[] video) {
    super(context, R.layout.custom_row, video);
    this.context = context;
    this.video = video;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {

    LayoutInflater videoInflator = LayoutInflater.from(getContext());
    View customView = videoInflator.inflate(R.layout.custom_row, parent, false);
        ViewHolder viewHolder = new ViewHolder();
        viewHolder.position = position;
        viewHolder.path = video[position];
        viewHolder.videoTitle = (TextView) customView.findViewById(R.id.videoTitle);
        viewHolder.videoThumbnail = (ImageView) customView.findViewById(R.id.videoThumbnail);
        //rowView.setTag(viewHolder);
    //}
    customView.setTag(viewHolder);


    final String videoItem = video[position];
    int index=videoItem.lastIndexOf('/');
    String lastString=(videoItem.substring(index +1));
    index = lastString.indexOf(".mp4");
    lastString=(lastString.substring(0,index));
    viewHolder.videoTitle.setText(lastString);

    new AsyncTask<ViewHolder, Void, Bitmap>() {
        private ViewHolder v;

        @Override
        protected Bitmap doInBackground(ViewHolder... params) {
            v = params[0];
            Bitmap thumb = ThumbnailUtils.createVideoThumbnail(videoItem, MediaStore.Images.Thumbnails.MINI_KIND);
            return thumb;
        }

        @Override
        protected void onPostExecute(Bitmap result) {
            super.onPostExecute(result);
            if (v.position == position) {
                // If this item hasn't been recycled already, hide the
                // progress and set and show the image
                v.videoThumbnail.setImageBitmap(result);
            }
        }
    }.execute(viewHolder);
    return customView;
}

}

Amit
  • 21
  • 3
1

Maybe you should try:

view = mInflater.inflate(mResource,parent,null);

Check this blog it explains the similar issue:

http://www.doubleencore.com/2013/05/layout-inflation-as-intended/

xav
  • 4,836
  • 7
  • 41
  • 55
0

Hey I found the solution to this problem just use following function instead of your function

 @Override
protected void onPostExecute(Bitmap result) {

    if (!imv.getTag().toString().equals(rec_id)) {
           return;
    }

    if(result != null && imv != null){
        int index = id.indexOf(imv.getTag().toString());
        if(list.getFirstVisiblePosition()<=index && index<=list.getLastVisiblePosition())
        {
            imv.setVisibility(View.VISIBLE);
            imv.setImageBitmap(result);
        }
    }else{
        imv.setImageBitmap(icon);
        imv.setVisibility(View.GONE);
    }
}

Here list is the object of listview. Just pass your list view object to your adapter and paste this function instead of your onPostExecute function.

Sandeep
  • 2,708
  • 20
  • 27
0

What I would do (unless you have thousands of images): 1. create a data structure - a simple class holding a String name to be displayed and a bitmap 2. create an adapter for it 3. in the getView method assign the correct bitmap to the correct ImageView.

In your case though you can create a similar data structure but holding not a bitmap but an AsyncTask. Anyway you need to bind the asynctask to the string into one item. An array (or arraylist) of such items will be fed to your adapter. Displayed will be an imageview and a textview.

AsyncTask can be cancelled with cancel().

Yar
  • 4,535
  • 2
  • 32
  • 40