James Oliver
James Oliver

Reputation: 19

Android - ListView images shuffled when scrolling

I'm trying to make a ListView with dynamically loaded images, using an AsyncTask to download the image and then set it into the ListView. My problem is that, while scrolling down, the images get randomly changed.

public class GetAllCustomerListViewAdapter extends BaseAdapter {

private JSONArray dataArray;
private Activity activity;

private static LayoutInflater inflater = null;
private static final String baseUrlForImage = "http://192.168.254.1/contact/images/";

public GetAllCustomerListViewAdapter(JSONArray jsonArray, Activity a)
{
    this.dataArray = jsonArray;
    this.activity = a;


    inflater = (LayoutInflater) this.activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}

@Override
public int getCount() {
    return this.dataArray.length();
}

@Override
public Object getItem(int position) {
    return position;
}

@Override
public long getItemId(int position) {
    return position;
}

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

    // set up convert view if it is null
    final ListCell cell;
    if (convertView == null)
    {
        convertView = inflater.inflate(R.layout.get_all_customer_list_view_cell, null);
        cell = new ListCell();

        cell.FullName = (TextView) convertView.findViewById(R.id.customer_full_name);
        cell.Age = (TextView) convertView.findViewById(R.id.customer_age);

        cell.mobile = (ImageView) convertView.findViewById(R.id.customer_mobile);

        convertView.setTag(cell);
    }
    else
    {
        cell = (ListCell) convertView.getTag();
    }

    // change the data of cell

    try
    {
        JSONObject jsonObject = this.dataArray.getJSONObject(position);
        cell.FullName.setText(jsonObject.getString("FirstName")+" "+jsonObject.getString("LastName"));
        cell.Age.setText(" "+jsonObject.getInt("Age"));

        String nameOfImage = jsonObject.getString("id");

        String urlForImageInServer = "http://192.168.254.1/contact/getImage.php?id="+nameOfImage;

        new AsyncTask<String, Void, Bitmap>() {

            @Override
            protected Bitmap doInBackground(String... params) {
                String url = params[0];
                Bitmap icon = null;

                try {
                    InputStream in = new java.net.URL(url).openStream();
                    icon = BitmapFactory.decodeStream(in);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                return icon;
            }

            protected void onPostExecute(Bitmap result) {
                cell.mobile.setImageBitmap(result);             
            }               


        }.execute(urlForImageInServer);           



    }
    catch (JSONException e)
    {
        e.printStackTrace();
    }


    return convertView;
}



private  class  ListCell
{
    private TextView FullName;
    private TextView Age;

    private ImageView mobile;

}    

}

Upvotes: 2

Views: 779

Answers (1)

MDragon
MDragon

Reputation: 1886

I just did this recently (at 1am actually)- the problem is that the ListView is actively recycling the views. Therefore, it gets the old views which still holds the bitmaps.

The following is what you need:

[Note: Check if the Bitmap was cached earlier. If not, do the steps below]

  1. Cache the Bitmap first before trying to load it
  2. Cache the targeted ImageView (and keep track of what Bitmap is supposed to go with what Bitmap). Also, empty out the ImageView just in case it still contains a Bitmap from a previous recycled item (imageView.setImageBitmap(null);)
  3. Double check that the cached ImageView has not already been cached for a previous item already. If already cached previously, then somehow state that the bitmap that is being received should not be loaded (I did this by storing an ImageView for every spot, and once the Bitmap is received, if that row's ImageView is null again then I don't load in the Bitmap)
  4. Load the Bitmap (as mentioned above) if you still should- ie, if the ImageView wasn't recycled again. As I mentioned earlier

The solution is albeit a bit complicated but works really well and smoothly if implemented correctly. If you'd like an example, feel free to check one out in action here in my current project.

Edit for requested example:

Here is some not-so-pseudo pseudocode. The main part you still need to do is actually implement the lazy loading system (which is a whole other topic). Once again, feel free to visit my project linked above and especially look at the "ImageLoaderNoCache" NetworkUtil.

public class GetAllCustomerListViewAdapter extends BaseAdapter {

    private JSONArray dataArray;
    private Activity activity;

    private static LayoutInflater inflater = null;
    private static final String baseUrlForImage = "http://192.168.254.1/contact/images/";

    // The list caches bitmaps as well as target imageViews, in order of the rows
    private ArrayList<RowViewData> rowViewDataList;

    public GetAllCustomerListViewAdapter(JSONArray jsonArray, Activity a)
    {
        this.dataArray = jsonArray;
        this.activity = a;


        inflater = (LayoutInflater) this.activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        // Initialize the list
        rowViewDataList = new ArrayList<RowViewData>(dataArray.length());

        // This is more of a preference, but add in every item for the rowViewDataList
        for(int i = 0; i < dataArray.length(); ++i) {
            // Create a new, empty rowViewData instance and put it into the list
            RowViewData rowData = new RowViewData();
            rowViewDataList.add(rowData);
        }
    }

    @Override
    public int getCount() {
        return this.dataArray.length();
    }

    @Override
    public Object getItem(int position) {
        return position;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

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

        // set up convert view if it is null
        final ListCell cell;
        if (convertView == null)
        {
            convertView = inflater.inflate(R.layout.get_all_customer_list_view_cell, null);
            cell = new ListCell();

            cell.FullName = (TextView) convertView.findViewById(R.id.customer_full_name);
            cell.Age = (TextView) convertView.findViewById(R.id.customer_age);

            cell.mobile = (ImageView) convertView.findViewById(R.id.customer_mobile);

            convertView.setTag(cell);
        }
        else
        {
            cell = (ListCell) convertView.getTag();
        }

        // change the data of cell

        try
        {
            JSONObject jsonObject = this.dataArray.getJSONObject(position);
            cell.FullName.setText(jsonObject.getString("FirstName")+" "+jsonObject.getString("LastName"));
            cell.Age.setText(" "+jsonObject.getInt("Age"));


            // Get the rowViewData for this position
            RowViewData curRowViewData = rowViewDataList.get(position);

            // If the bitmap is already cached, just use it
            if(curRowViewData.bitmap != null) {
                cell.mobile.setImageBitmap(curRowViewData.bitmap);
            }
            // Otherwise, gotta do some real work
            else {
                // First, set the cell's imageView's bitmap to null
                //      [in case this is a recycled view already with a bitmap]
                cell.mobile.setImageBitmap(null);

                // Then, start up the caching system
                String nameOfImage = jsonObject.getString("id");
                String urlForImageInServer = "http://192.168.254.1/contact/getImage.php?id="+nameOfImage;

                // Create a system to load images lazily and then receive the bitmaps
                // Look at my ImageLoaderNoCache for an example. Feel free to reuse it
                /**
                 * this = this adapter should implement an interface. Look below for the corresponding method
                 * url = image source url
                 * position = position this bitmap belongs to. Will get returned along with bitmap for reference
                 */
                LazyImageLoader.GetBitmap(this, url, position);

                // Cache/target this imageView for later
                curRowViewData.targetImageView = cell.mobile;

                // Double check this imageView is not being targeted already elsewhere
                //      ie, one of the recycling fixes [else, two bitmaps would load at once, etc]
                //  Also, probably could be more efficient
                for(int i = 0; i < rowViewDataList.size(); ++i) {
                    // If this is the current position, then skip all the logic for this round
                    if(i == position) continue;

                    // Get the rowViewData for this round
                        // Yeah... I should've used "cell" instead of row for you...
                    RowViewData checkRowData = rowViewDataList.get(i);

                    // If the targeted ImageView is the same, null-ify it
                    if(checkRowData.targetImageView == curRowViewData.targetImageView) {
                        // The old one should be null as the bitmap should not load suddenly into
                        //      the current recycled item
                        checkRowData.targetImageView = null;

                        // Personally, I knew that there would only be one copy at any time
                        //      so I tried to save time by breaking out here. Feel free to be
                        //          cautious and comment out the break statement
                        break;
                    }
                }
            }
        }
        catch (JSONException e)
        {
            e.printStackTrace();
        }


        return convertView;
    }

    /**
     * Should be called as part of an interface with the LazyImageLoader
     * @param bitmap - The lazily loaded bitmap that was requested earlier
     * @param position - The row/cell position that the bitmap was requested for
     */
    public void getBitmapForPosition(Bitmap bitmap, int position) {
        // Get the rowViewData instance for this row/position
        RowViewData curRowData = rowViewDataList.get(position);

        // Cache the bitmap
        curRowData.bitmap = bitmap;

        // Check if the bitmap should be loaded still
        if(curRowData.targetImageView != null) {
            curRowData.targetImageView.setImageBitmap(bitmap);

            // Not sure if strictly necessary, but good practice to make the cached view null now
            curRowData.targetImageView = null;
        }
    }

    private  class  ListCell
    {
        private TextView FullName;
        private TextView Age;

        private ImageView mobile;

    }

    // Holds all the data for lazily loading an image with a bitmap
    private class RowViewData {
        public ImageView targetImageView;
        public Bitmap bitmap;
    }
}

Upvotes: 1

Related Questions