Android实现ListView异步加载图片

http://blog.trifork.nl/2009/09/17/exploring-the-world-of-android-part-2/

A simple example

To give you a simple example of a case in which you need to write your own ListAdapter: displaying a list of images with some text next to it.


Example of a ListView containing Youtube search results in the form of images and text

The images need to be on-the-fly downloaded from the internet. Let's create a class which represents items in the list:

public class ImageAndText {
    private String imageUrl;
    private String text;

    public ImageAndText(String imageUrl, String text) {
        this.imageUrl = imageUrl;
        this.text = text;
    }
    public String getImageUrl() {
        return imageUrl;
    }
    public String getText() {
        return text;
    }
}

Now, let's create an implementation of a ListAdapter that is able to display a list of theseImageAndTexts.

public class ImageAndTextListAdapter extends ArrayAdapter<ImageAndText> {

    public ImageAndTextListAdapter(Activity activity, List<ImageAndText> imageAndTexts) {
        super(activity, 0, imageAndTexts);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Activity activity = (Activity) getContext();
        LayoutInflater inflater = activity.getLayoutInflater();

        // Inflate the views from XML
        View rowView = inflater.inflate(R.layout.image_and_text_row, null);
        ImageAndText imageAndText = getItem(position);

        // Load the image and set it on the ImageView
        ImageView imageView = (ImageView) rowView.findViewById(R.id.image);
        imageView.setImageDrawable(loadImageFromUrl(imageAndText.getImageUrl()));

        // Set the text on the TextView
        TextView textView = (TextView) rowView.findViewById(R.id.text);
        textView.setText(imageAndText.getText());

        return rowView;
    }

    public static Drawable loadImageFromUrl(String url) {
        InputStream inputStream;
        try {
            inputStream = new URL(url).openStream();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return Drawable.createFromStream(inputStream, "src");
    }
}

The views are inflated from an XML file called "image_and_text_row.xml":

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content">

        <ImageView android:id="@+id/image"
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:src="@drawable/default_image"/>

        <TextView android:id="@+id/text"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"/>

</LinearLayout>

This ListAdapter implementation renders the ImageAndTexts in the ListView like you would expect. The only thing is that this only works for a very small list which doesn't require scrolling to see all items. If the list of ImageAndTexts
gets bigger you will notice that scrolling isn't as smooth as it should be (in fact, it's far off!).

Improving performance

The biggest bottleneck in the above example is the fact that the images have to be downloaded from the internet. Because we execute all our code in the same thread as the UI, the UI will get stuck each time an image is being downloaded. If you run the same
application using a 3G internet connection instead of WiFi, the performance will even be worse.

To avoid this we want the image to be loaded in a separate thread to not disturb the UI thread too much. To make this happen, we could use an AsyncTask which
is designed for cases like this. But in practice, you will notice that the AsyncTask is limited to 10 threads. This number is hardcoded somewhere in the Android SDK so we cannot change this. In this case it's a limitation we cannot live with, because
often more than 10 images are loaded at the same time.

AsyncImageLoader

An alternative is to manually spawn a new Thread for each image. In addition we should useHandlers
to deliver the downloaded images to the UI thread. We want to do this because only from the UI thread you are allowed to modify the UI (read: draw an image on the screen). I created a class called AsyncImageLoader which takes care of loading images
using Threads and Handlerslike I just described. Also it caches images to avoid a single image to be downloaded multiple times.

public class AsyncImageLoader {
    private HashMap<String, SoftReference<Drawable>> imageCache;

    public AsyncImageLoader() {
    	drawableMap = new HashMap<String, SoftReference<Drawable>>();
    }

    public Drawable loadDrawable(final String imageUrl, final ImageCallback imageCallback) {
    	if (drawableMap.containsKey(imageUrl)) {
            SoftReference<Drawable> softReference = imageCache.get(imageUrl);
            Drawable drawable = softReference.get();
            if (drawable != null) {
                return drawable;
            }
    	}
    	final Handler handler = new Handler() {
    		@Override
    		public void handleMessage(Message message) {
                imageCallback.imageLoaded((Drawable) message.obj, imageUrl);
    		}
    	};
    	new Thread() {
    		@Override
    		public void run() {
                Drawable drawable = loadImageFromUrl(imageUrl);
                imageCache.put(imageUrl, new SoftReference<Drawable>(drawable));
                Message message = handler.obtainMessage(0, drawable);
                handler.sendMessage(message);
    		}
    	}.start();
        return null;
    }

    public static Drawable loadImageFromUrl(String url) {
        // ...
    }

    public interface ImageCallback {
        public void imageLoaded(Drawable imageDrawable, String imageUrl);
    }
}

Notice that I used a SoftReference for caching images, to allow the garbage collector to clean the images from the cache
when needed. How it works:

  • Call loadDrawable(imageUrl, imageCallback) providing an anonymous implementation of theImageCallback interface
  • If the image doesn't exist in the cache yet, the image is downloaded in a separate thread and the ImageCallback is called as soon as the download is complete.
  • If the image DOES exist in the cache, it is immediately returned and the ImageCallback is never called.

Only one instance of AsyncImageLoader should exist in your application, or else the caching won't work. If we take the example ImageAndTextListAdapter class we can now replace:

ImageView imageView = (ImageView) rowView.findViewById(R.id.image);
imageView.setImageDrawable(loadImageFromUrl(imageAndText.getImageUrl()));

with:

final ImageView imageView = (ImageView) rowView.findViewById(R.id.image);
Drawable cachedImage = asyncImageLoader.loadDrawable(imageAndText.getImageUrl(), new ImageCallback() {
    public void imageLoaded(Drawable imageDrawable, String imageUrl) {
        imageView.setImageDrawable(imageDrawable);
    }
});
imageView.setImageDrawable(cachedImage);

Using this approach, the ListView performs a lot better and feels much more smooth because the UI thread is no longer blocked by the loading of images.

Improve the performance even more

If you tried the solution described above you will notice that the ListView is still not a 100% smooth. You will still notice some little disruptions that make it a little less smooth than it could be. There are two things remaining that can be improved:

  • the expensive call to findViewById()
  • inflating the entire row from XML every time

The solution is obvious: we should cache/reuse these things! Mark Murphy did a very nice job on writing a few blog entries describing how this can be done. To reuse the views which are inflated from XML read this blog entry:
http://www.androidguys.com/2008/07/17/fancy-listviews-part-two/

To cache the views returned by findViewById() read this blog entry:
http://www.androidguys.com/2008/07/22/fancy-listviews-part-three/

If we apply the strategies described in Mark Murphy's blog entries our ImageAndTextListAdaptercould look like the following:

public class ImageAndTextListAdapter extends ArrayAdapter<ImageAndText> {

    private ListView listView;
    private AsyncImageLoader asyncImageLoader;

    public ImageAndTextListAdapter(Activity activity, List<ImageAndText> imageAndTexts, ListView listView) {
        super(activity, 0, imageAndTexts);
        this.listView = listView;
        asyncImageLoader = new AsyncImageLoader();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Activity activity = (Activity) getContext();

        // Inflate the views from XML
        View rowView = convertView;
        ViewCache viewCache;
        if (rowView == null) {
            LayoutInflater inflater = activity.getLayoutInflater();
            rowView = inflater.inflate(R.layout.image_and_text_row, null);
            viewCache = new ViewCache(rowView);
            rowView.setTag(viewCache);
        } else {
            viewCache = (ViewCache) rowView.getTag();
        }
        ImageAndText imageAndText = getItem(position);

        // Load the image and set it on the ImageView
        String imageUrl = imageAndText.getImageUrl();
        ImageView imageView = viewCache.getImageView();
        imageView.setTag(imageUrl);
        Drawable cachedImage = asyncImageLoader.loadDrawable(imageUrl, new ImageCallback() {
            public void imageLoaded(Drawable imageDrawable, String imageUrl) {
                ImageView imageViewByTag = (ImageView) listView.findViewWithTag(imageUrl);
                if (imageViewByTag != null) {
                    imageViewByTag.setImageDrawable(imageDrawable);
                }
            }
        });
        imageView.setImageDrawable(cachedImage);

        // Set the text on the TextView
        TextView textView = viewCache.getTextView();
        textView.setText(imageAndText.getText());

        return rowView;
    }
}

There are two things to notice. The first thing is that the drawable is not directly set to theImageView anymore after loading. Instead, the right ImageView is looked up through it's tag. This is done because we're now reusing views and the
images might end up on the wrong rows. We need a reference to the ListView to lookup ImageViews by tag.

The other thing to notice, is that this implementation uses an object called ViewCache. This is what the class for that object looks like:

public class ViewCache {

    private View baseView;
    private TextView textView;
    private ImageView imageView;

    public ViewCache(View baseView) {
        this.baseView = baseView;
    }

    public TextView getTextView() {
        if (textView == null) {
            textView = (TextView) baseView.findViewById(R.id.text);
        }
        return titleView;
    }

    public ImageView getImageView() {
        if (imageView == null) {
            imageView = (ImageView) baseView.findViewById(R.id.image);
        }
        return imageView;
    }
}

This ViewCache is the same as what Mark Murphy calls a "ViewWrapper" and takes care of caching individual views which normally would have to be looked up every time using the expensive call to findViewById().

To summarize

I've shown you how to improve performance of a ListView in three different ways:

  • By loading images in a seperate thread
  • By reusing rows in the list
  • By caching views within a row
时间: 2024-11-01 23:19:22

Android实现ListView异步加载图片的相关文章

Android实现ListView异步加载图片的方法_Android

本文实例讲述了Android实现ListView异步加载图片的方法.分享给大家供大家参考.具体如下: ListView异步加载图片是非常实用的方法,凡是是要通过网络获取图片资源一般使用这种方法比较好,用户体验好,不用让用户等待下去,下面就说实现方法,先贴上主方法的代码: package cn.wangmeng.test; import java.io.IOException; import java.io.InputStream; import java.lang.ref.SoftReferen

Android中ListView异步加载图片错位、重复、闪烁问题分析及解决方案

Android ListView异步加载图片错位.重复.闪烁分析以及解决方案,具体问题分析以及解决方案请看下文. 我们在使用ListView异步加载图片的时候,在快速滑动或者网络不好的情况下,会出现图片错位.重复.闪烁等问题,其实这些问题总结起来就是一个问题,我们需要对这些问题进行ListView的优化. 比如ListView上有100个Item,一屏只显示10个Item,我们知道getView()中convertView是用来复用View对象的,因为一个Item的对应一个View对象,而Ima

Android之ListView异步加载图片且仅显示可见子项中的图片

折腾了好多天,遇到 N 多让人崩溃无语的问题,不过今天终于有些收获了,这是实验的第一版,有些混乱,下一步进行改造细分,先把代码记录在这儿吧. 网上查了很多资料,发现都千篇一律,抄来抄去,很多细节和完整实例都没看到,只有自己一点点研究了,总体感觉 android 下面要显示个图片真不容易啊. 项目主要实现的功能: 异步加载图片 图片内存缓存.异步磁盘文件缓存 解决使用 viewHolder 后出现的图片错位问题 优化列表滚动性能,仅显示可见子项中的图片 无需固定图片显示高度,对高度进行缓存使列表滚

Android ListView异步加载图片方法详解_Android

本文实例讲述了Android ListView异步加载图片方法.分享给大家供大家参考,具体如下: 先说说这篇文章的优点把,开启线程异步加载图片,然后刷新UI显示图片,而且通过弱引用缓存网络加载的图片,节省了再次连接网络的开销. 这样做无疑是非常可取的方法,但是加载图片时仍然会感觉到轻微的卡屏现象,特别是listview里的item在进行快速滑动的时候. 我找了一下原因,可能是在listview快速滑动屏幕的时候划过的item太多 而且每次调用getView方法后就会异步的在过去某个时间内用han

解决Android ListView异步加载图片乱序问题

在Android所有系统自带的控件当中,ListView这个控件算是用法比较复杂的了,关键是用法复杂也就算了,它还经常会出现一些稀奇古怪的问题,让人非常头疼.比如说在ListView中加载图片,如果是同步加载图片倒还好,但是一旦使用异步加载图片那么问题就来了,这个问题我相信很多Android开发者都曾经遇到过,就是异步加载图片会出现错位乱序的情况.遇到这个问题时,不少人在网上搜索找到了相应的解决方案,但是真正深入理解这个问题出现的原因并对症解决的人恐怕还并不是很多.那么今天我们就来具体深入分析一

android开发之ListView异步加载图片

ListView异步加载图片是非常实用的方法,凡是是要通过网络获取图片资源一般使用这种方法比较好,用户体验好,不用让用户等待下去,下面就说实现方法,先贴上主方法的代码:  代码如下 复制代码 package cn.wangmeng.test;   import java.io.IOException; import java.io.InputStream; import java.lang.ref.SoftReference; import java.net.MalformedURLExcept

Android ListView异步加载图片方法详解

本文实例讲述了Android ListView异步加载图片方法.分享给大家供大家参考,具体如下: 先说说这篇文章的优点把,开启线程异步加载图片,然后刷新UI显示图片,而且通过弱引用缓存网络加载的图片,节省了再次连接网络的开销. 这样做无疑是非常可取的方法,但是加载图片时仍然会感觉到轻微的卡屏现象,特别是listview里的item在进行快速滑动的时候. 我找了一下原因,可能是在listview快速滑动屏幕的时候划过的item太多 而且每次调用getView方法后就会异步的在过去某个时间内用han

listview异步加载图片并防止错位

android listview 异步加载图片并防止错位 网上找了一张图, listview 异步加载图片之所以错位的根本原因是重用了 convertView 且有异步操作. 如果不重用 convertView 不会出现错位现象, 重用 convertView 但没有异步操作也不会有问题. 我简单分析一下: 当重用 convertView 时,最初一屏显示 7 条记录, getView 被调用 7 次,创建了 7 个 convertView. 当 Item1 划出屏幕, Item8 进入屏幕时,

ListView异步加载图片(解决图片混淆)

代码下载地址: http://115.com/file/e75ks6jj#ImageLoader_test.zip        由于工作原因,很久没有写博客了,工作中经常遇到ListView异步加载图片的问题,国内的网站上查了N多资料,几乎没有一个可用的,最根本的图片混淆问题都没有得到充分地解决.我的这个例子是借鉴Google Code中的例子,删除了其中的没有必要的代码,完全可行.   该工程由ImageListActivity.ImageAdapter.ImageDownloader三个类