安卓:在ListView中异步加载图片

3
我想展示一个ListView,使用自定义的适配器(包括图片和文本)。
由于图片需要从远程服务器加载,因此我决定使用AsyncTask。
实际上,图片已经成功地显示了,但如果我快速滚动列表,在加载完成前会显示错误的图片,之后才会显示正确的图片。
以下是我的适配器代码:
public class GiAdapter extends BaseAdapter {

    private Context mContext;

    private List<SiteStaff> mListAppInfo;
    private HashMap<Integer, ImageView> views;
    private HashMap<String,Bitmap> oldPicts = new  HashMap<String,Bitmap>();
    private LayoutInflater mInflater;
    private boolean auto;

    private final String BUNDLE_URL = "url";
    private final String BUNDLE_BM = "bm";
    private final String BUNDLE_POS = "pos";
    private final String BUNDLE_ID = "id";

    public GiAdapter(Context context, List<SiteStaff> list) {
        mContext = context;
        mListAppInfo = list;
        views = new HashMap<Integer, ImageView>();
        mInflater = LayoutInflater.from(mContext);

    }

    @Override
    public int getCount() {
        return mListAppInfo.size();
    }

    @Override
    public Object getItem(int position) {
        return mListAppInfo.get(position).getId();
    }

    @Override
    public long getItemId(int position) {
        return mListAppInfo.get(position).getId();
    }

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

        LinearLayout layoutItem;

        // reuse of convertView
        if (convertView == null) {
            layoutItem = (LinearLayout) mInflater.inflate(R.layout.auto_gi, parent, false);
        } else {
            layoutItem = (LinearLayout) convertView;
        }

        // infos for the current element
        SiteStaff entry = mListAppInfo.get(position);

        //set some text fields
        TextView name = (TextView) layoutItem.findViewById(R.id.name);
        TextView size = (TextView) layoutItem.findViewById(R.id.size); 

        name.setText(entry.getName());
        size.setText(entry.getSize());

        // get the imageView for the current object
        ImageView v = (ImageView) layoutItem.findViewById(R.id.gi_image);

        // put infos in bundle and send to the LoadImage class
        Bundle b = new Bundle();

        //url of the pict
        b.putString(BUNDLE_URL, entry.getUrl());

        //position in the listView
        b.putInt(BUNDLE_POS, position);

        //id of the current object
        b.putInt(BUNDLE_ID, entry.getId());

        //put info in the map in order to display in the onPostExecute
        views.put(position, v);

        // thread
        new LoadImage().execute(b);

        return layoutItem;

    }

    //asyncTackClass for loadingpictures
    private class LoadImage extends AsyncTask<Bundle, Void, Bundle> {

        @Override
        protected Bundle doInBackground(Bundle... b) {

            Bitmap bm =null;

            //cache: for better performance, check if url alredy exists
            if(oldPicts.get(b[0].getString(BUNDLE_URL))==null){
                bm = Utils.getBitMapFromUrl(b[0].getString(BUNDLE_URL));
                oldPicts.put(b[0].getString(BUNDLE_URL),bm);
            }else{
                bm = oldPicts.get(b[0].getString(BUNDLE_URL));
            }

            // get info from bundle
            Bundle bundle = new Bundle();
            bundle.putParcelable(BUNDLE_BM, bm);
            bundle.putInt(BUNDLE_POS, b[0].getInt(BUNDLE_POS));

            return bundle;

        }

        @Override
        protected void onPostExecute(Bundle result) {
            super.onPostExecute(result);

            //get picture saved in the map + set
            ImageView view = views.get(result.getInt(BUNDLE_POS));
            Bitmap bm = (Bitmap) result.getParcelable(BUNDLE_BM);

            if (bm != null){ //if bitmap exists...
                view.setImageBitmap(bm);
            }else{ //if not picture, display the default ressource
                view.setImageResource(R.drawable.unknow);
            }

        }

    }

}

感谢您!
3个回答

6
像Shubhayu提到的那样,您在ListView行中看到错误的图像,是因为您的适配器正在回收视图以构建每行,并且您的Async Tasks正在维护对该回收视图的引用。当您的任务完成时,它们将更新ImageView,但实际上此时可能对应于其他某一行。
每次调用getView时充气新视图的问题在于当滚动时,您的ListView的性能会变得很差。您(正确地)使用convertView来减少这种开销,只是缺少正确将ImageView与Async联系起来的部分。我在这里找到了一个解决方案:http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html 简而言之,它基本上是扩展Drawable以封装任务,并仅在任务适当地与图像绑定时更新图像。 "处理并发"部分解决了您面临的问题。特别是,在该部分之前阅读段落以概括导致您问题的原因。

5
这也曾经发生在我身上。我发现,在新图片绑定到ImageView之前,旧图片会显示一段时间。在你的情况下,正确的信息会在onPostExecute()中显示,这需要一些时间,在此之前,convertview中设置的任何数据都会被显示出来。有两种方法可以解决这个问题:
在你的getView()中,将if{}... else{}块更改为:
layoutItem = (LinearLayout) mInflater.inflate(R.layout.auto_gi, parent, false);

在getView()中设置imageView为占位图,直到onPostExecute()被调用。希望这可以帮助你。

谢谢您!我已经尝试了您的代码,图片完美地显示出来了! - johann
3
注意,如果您采用第一种方法,实际上会为列表视图中的每一行增加视图(这是一项相当昂贵的操作),这意味着您的ListView将无法滚动得很顺畅。 - Christian

1
当您扩展BaseAdapter并实现getView(int position, View convertView, ViewGroup parent)时,convertView实际上是一个已回收的视图。
它是被销毁的视图(最后一个被推出屏幕的视图)。您可以清除convertView,以便它不显示旧数据。也许找到带有图像的视图并执行layoutItem.removeAll()。希望这样可以清除视图并为您提供一个干净的布局来使用。

谢谢!它帮助我理解了BaseAdapter类的逻辑。 - johann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接