h4ck3d
h4ck3d

Reputation: 6364

Multi-threading issues during image loading

In my app , there is a ListView which uses a custom CursorAdapter to load data into it which is basically an ImageView and a TextView . I am loading images in an AsyncTask , now the problem is , initially all images are assigned to correct text , but when i scroll up and down quickly , it binds random image for each text . Since I am using CursorAdapter , I can't use ViewHolder here , so what shall i do to eliminate this problem?

Here is my sample code :

public class MyAdapter extends CursorAdapter {

    private LayoutInflater mLayoutInflater;
    @SuppressWarnings("unused")
    private Context mContext;

    public MyAdapter(Context context, Cursor c, int flags) {
        super(context, c, flags);
        mContext = context;
        mLayoutInflater = LayoutInflater.from(context);
    }
    public void bindView(View view, Context context, Cursor cursor) {
        String title = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.TITLE));
        String album_id = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID));
        TextView text = (TextView)view.findViewById(R.id.txtTitle);
        text.setText(title);
        Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");
        Uri uri = ContentUris.withAppendedId(sArtworkUri, Integer.valueOf(album_id));
        new MyImageLoader(context,view).execute(uri);

    }
    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        return mLayoutInflater.inflate(R.layout.row, parent, false);
    }
    private class MyImageLoader extends AsyncTask<Uri, Void, Bitmap>{
        Context context;
        View view;


        MyImageLoader(Context context,View view){
            this.context = context;
            this.view = view;
        }
        @Override
        protected Bitmap doInBackground(Uri... uri) {
            ContentResolver res = context.getContentResolver();
            InputStream in = null;
            try {
                in = res.openInputStream(uri[0]);
            } 
            catch (FileNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            Bitmap artwork = BitmapFactory.decodeStream(in);
            return artwork;
        }
        protected void onPostExecute(Bitmap bmp){
            ImageView iv = (ImageView)view.findViewById(R.id.imgIcon);
            if(bmp!=null)
                //iv.setImageBitmap(bmp);
                iv.setImageBitmap(Bitmap.createScaledBitmap(bmp, 100, 100, false));
        }
    }
}

UPDATED : I applied the setTag method , now the shuffling is less , however when I scroll fast the old image persists for a second until the correct image is loaded. Here is the new code :

public void bindView(View view, Context context, Cursor cursor) {
        String title = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.TITLE));
        String album_id = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID));
        ImageView iv = (ImageView)view.findViewById(R.id.imgIcon);
        TextView text = (TextView)view.findViewById(R.id.txtTitle);
        text.setText(title);
        Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");
        Uri uri = ContentUris.withAppendedId(sArtworkUri, Integer.valueOf(album_id));
// *****TAG SET*****
        iv.setTag(uri);
//***PASSING BOTH URI AND IMAGEVIEW TO CONSTRUCTOR***
        new MyImageLoader(context,view,iv,uri).execute(uri);

    }
    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        View v = mLayoutInflater.inflate(R.layout.row, parent, false);
        //v.setTag(R.id.imgIcon, v.findViewById(R.id.imgIcon));
        return v;
    }
    private class MyImageLoader extends AsyncTask<Object, Void, Bitmap>{
        Context context;
        View v;
        ImageView iv;
        Uri u;

        MyImageLoader(Context context,View v,ImageView iv,Uri u){
            this.context = context;
            this.v = v;
            this.iv = iv;   
            this.u = u;
        }
        @Override

        protected synchronized Bitmap doInBackground(Object... param) {
            ContentResolver res = context.getContentResolver();
            InputStream in = null;
            try {
                Uri uri= (Uri)param[0];
                in = res.openInputStream(uri);
            } 
            catch (FileNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            Bitmap artwork = BitmapFactory.decodeStream(in);

            return artwork;
        }
        protected void onPostExecute(Bitmap bmp){

            if(bmp!=null)
            {   ImageView iv = (ImageView)v.findViewById(R.id.imgIcon);
                if(iv.getTag().toString().equals(u.toString()))
                    iv.setImageBitmap(Bitmap.createScaledBitmap(bmp, 100, 100, false));
            }
        }
    }

UPDATE : Setting a placeholder image before calling background task makes it much better.

Upvotes: 1

Views: 430

Answers (1)

CommonsWare
CommonsWare

Reputation: 1006744

One solution that I have used is to attach the Uri to the ImageView via setTag(). Then, when you go to to update the ImageView, check to see whether the Uri of the image you are about to apply matches the value from getTag(). If it does, go ahead and update the ImageView. If it does not, the ImageView was recycled, and you can skip the update.

Another approach is to use setHasTransientState(true) on the ImageView (or perhaps on the row -- haven't tried this) when you decide you have a cache miss and need to kick off an AsyncTask. This will cause AdapterView to avoid recycling that row, until a matching setHasTransientState(false) call has been made. Chet Haase has a DevBytes video on this, in the context of applying an animation to a ListView row and trying to avoid recycling problems. However, setHasTransientState() is new to API Level 16, so if you are trying to support Android 4.0 or older devices, this will not be an available option.

Upvotes: 2

Related Questions