使用从互联网下载的图像延迟加载 GridView
Posted
技术标签:
【中文标题】使用从互联网下载的图像延迟加载 GridView【英文标题】:Lazy loading GridView with images downloading from internet 【发布时间】:2013-03-14 11:56:18 【问题描述】:多年来我一直在访问 Stack Overflow,这是我第一次找不到任何可以解决我问题的帖子(至少我没有看到任何帖子)。
我有一个带有自定义适配器的GridView
,我已将其覆盖以返回由ImageView
和TextView
创建的自定义视图。
我从带有AsyncTask
的URL 中进行JSON 解析后加载图像,将所有信息存储到doInBackground()
方法中的ArrayList
中,并在onPostExecute()
方法中调用notifyDataSetChanged()
。一切都很好。
现在我的问题是,当我启动活动时,网格视图需要 5-10 秒的时间才能创建并以实体形式呈现给用户。我想知道是否有一种方法可以先显示带有文本信息的网格视图,然后再加载每个图像。这是否可能,因为它们都是以相同的方法创建的?
@Override
public View getView(int arg0, View arg1, ViewGroup arg2)
View v = null;
if (arg1 == null)
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
v = inflater.inflate(R.layout.custom_product_view, null);
else
v = arg1;
iv = (ImageView) v.findViewById(R.id.product_image);
imageLoader.DisplayImage(products.get(arg0).getImage(), iv);
TextView tv = (TextView) v.findViewById(R.id.product_price);
tv.setText(products.get(arg0).getPrice());
return v;
我还必须通知您,正如您从 DisplayImage()
方法中看到的那样,我已经实现了这个延迟加载:Lazy load of images in ListView。它工作正常,但问题是它再次加载了整个视图。我想要做的是启动活动,首先加载标题,然后在完成下载后加载图像。使用这里的代码,它只是延迟加载网格视图的每个单元格包含的整个视图。我赢得了几秒钟,因为我没有像以前那样一次下载所有图像,但它仍然不是我要搜索的。
非常感谢。
【问题讨论】:
【参考方案1】:遵循这种方法。
首先,创建一个自定义的WebImageView类,如下所示。
public class WebImageView extends ImageView
private Drawable placeholder, image;
public WebImageView(Context context)
super(context);
public WebImageView(Context context, AttributeSet attrs, int defStyle)
super(context, attrs, defStyle);
public WebImageView(Context context, AttributeSet attrs)
super(context, attrs);
public void setPlaceholderImage(Drawable drawable)
placeholder = drawable;
if (image == null)
setImageDrawable(placeholder);
public void setPlaceholderImage(int resid)
placeholder = getResources().getDrawable(resid);
if (image == null)
setImageDrawable(placeholder);
public void setImageUrl(String url)
DownloadTask task = new DownloadTask();
task.execute(url);
private class DownloadTask extends AsyncTask<String, Void, Bitmap>
@Override
protected Bitmap doInBackground(String... params)
String url = params[0];
try
URLConnection conn = (new URL(url)).openConnection();
InputStream is = conn.getInputStream();
BufferedInputStream bis = new BufferedInputStream(is);
ByteArrayBuffer baf = new ByteArrayBuffer(50);
int current = 0;
while ((current=bis.read()) != -1)
baf.append((byte)current);
byte[] imageData = baf.toByteArray();
return BitmapFactory.decodeByteArray(imageData, 0, imageData.length);
catch (Exception e)
return null;
@Override
protected void onPostExecute(Bitmap result)
image = new BitmapDrawable(result);
if (image != null)
setImageDrawable(image);
接下来,在Activity中使用上面自定义的ImageView如下:
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
WebImageView imageView = (WebImageView) findViewById(R.id.webimage);
imageView.setPlaceholderImage(R.drawable.ic_launcher);
imageView.setImageUrl("http://www.google.co.in/images/srpr/logo3w.png");
简而言之,您正在为 ImageView 设置一个占位符图像,下载完成后该图像将被实际图像替换。所以 GridView 会立即渲染,不会有延迟。
实施细节: 因此,在您的自定义视图(带有图像 + 文本)中,不要使用简单的 ImageView,而是使用如上所示的 WebImageView。当您获得 JSON 响应时,将 TextView 设置为标题,将 WebImageView 设置为图像 url。 所以标题会立即显示,图片会延迟加载。
【讨论】:
是的,但是我解释过如何首先加载文本? 我的理解是您正在调用一些 REST api,然后解析 JSON 响应以获取图像 url 及其标题。因此,标题和图像都会有滞后。我提到的方法是拥有一个占位符文本和图像,以便渲染 GridView。然后按照我的示例或您为图像所做的方式异步更新标题和图像。【参考方案2】:我已经使用下面的类来实现图像的延迟加载,它对我来说非常棒。你也试试。
图像加载器
/** * This is class for display image in lazy-loading way. */ public class ImageLoader private static final String TAG = ImageLoader.class.getSimpleName(); private InputStream m_is = null; private OutputStream m_os = null; private Bitmap m_bitmap = null; private String m_imagePath; private File m_cacheDir; private WeakHashMap<String, Bitmap> m_cache = new WeakHashMap<String, Bitmap>(); /** * Makes the background thread low priority. This way it will not affect the * UI performance.<br> * Checks the Device SD card exits or not and assign path according this * condition. * * @param p_context * activity context */ public ImageLoader(Context p_context) /** * Make the background thread low priority. This way it will not affect * the UI performance */ m_imageLoaderThread.setPriority(Thread.NORM_PRIORITY - 1); /** * Check the Device SD card exits or not and assign path according this * condition. */ if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) m_imagePath = Environment.getExternalStorageDirectory() + "/Android/data/" + p_context.getPackageName(); m_cacheDir = new File(m_imagePath); else m_cacheDir = new File(p_context.getDir("Cache", Context.MODE_PRIVATE), "Cache"); if (!m_cacheDir.exists()) m_cacheDir.mkdirs(); /** * Check Image exits on HashMap or not.If exist then set image to ImageView * else send request in the queue. * * @param p_url * image Url * @param p_imageView * image container * @param p_prgBar * progressbar that is displayed till image is not download from * server. */ public void DisplayImage(String p_url, ImageView p_imageView, ProgressBar p_prgBar) throws CustomException if (m_cache.containsKey(p_url)) p_prgBar.setVisibility(View.GONE); p_imageView.setVisibility(View.VISIBLE); p_imageView.setImageBitmap(m_cache.get(p_url)); else queueImage(p_url, p_imageView, p_prgBar); /** * Clear old task from the queue and add new image downloading in the queue. * * @param p_url * image Url * @param p_imageView * image container * @param p_prgBar * progressbar that is displayed till image is not download from * server. */ private void queueImage(String p_url, ImageView p_imageView, ProgressBar p_prgBar) throws CustomException try m_imagesQueue.Clean(p_imageView); ImageToLoad m_photoObj = new ImageToLoad(p_url, p_imageView, p_prgBar); synchronized (m_imagesQueue.m_imagesToLoad) m_imagesQueue.m_imagesToLoad.push(m_photoObj); m_imagesQueue.m_imagesToLoad.notifyAll(); /** * start thread if it's not started yet */ if (m_imageLoaderThread.getState() == Thread.State.NEW) m_imageLoaderThread.start(); catch (CustomException c) throw c; catch (Throwable t) CustomLogHandler.printErrorlog(t); throw new CustomException(TAG + " Error in queueImage(String p_url, ImageView p_imageView, ProgressBar p_prgBar) of ImageLoader", t); /** * Checks in SD card for cached file.If bitmap is not available then will * download it from Url. * * @param p_url * imgae Url * @return bitmap from Cache or from server. */ private Bitmap getBitmap(String p_url) throws CustomException System.gc(); String m_fileName = String.valueOf(p_url.hashCode()); File m_file = new File(m_cacheDir, m_fileName); // from SD cache m_bitmap = decodeFile(m_file); if (m_bitmap != null) return m_bitmap; // from web try Bitmap m_bitmap = null; int m_connectionCode = 0; m_connectionCode = HttpConnection.getHttpUrlConnection(p_url).getResponseCode(); if (m_connectionCode == HttpURLConnection.HTTP_OK) m_is = new URL(p_url).openStream(); m_os = new FileOutputStream(m_file); FileIO.copyStream(m_is, m_os); m_os.close(); m_os = null; m_bitmap = decodeFile(m_file); m_is.close(); m_is = null; HttpConnection.getHttpUrlConnection(p_url).disconnect(); return m_bitmap; catch (CustomException c) throw c; catch (Throwable t) CustomLogHandler.printErrorlog(t); throw new CustomException(TAG + " Error in getBitmap(String p_url) of ImageLoader", t); /** * Decodes the Image file to bitmap. * * @param p_file * Image file object * @return decoded bitmap */ private Bitmap decodeFile(File p_file) throws CustomException try // decode image size Bitmap m_retBmp = null; System.gc(); int m_scale = 1; if (p_file.length() > 400000) m_scale = 4; else if (p_file.length() > 100000 && p_file.length() < 400000) m_scale = 3; // decode with inSampleSize if (p_file.exists()) BitmapFactory.Options m_o2 = new BitmapFactory.Options(); m_o2.inSampleSize = m_scale; m_retBmp = BitmapFactory.decodeFile(p_file.getPath(), m_o2); return m_retBmp; catch (Throwable t) CustomLogHandler.printErrorlog(t); throw new CustomException(TAG + " Error in decodeFile(File p_file) of ImageLoader", t); /** * Stores image information */ private class ImageToLoad public String m_url; public ImageView m_imageView; public ProgressBar m_prgBar; public ImageToLoad(String p_str, ImageView p_img, ProgressBar p_prgBar) m_url = p_str; m_imageView = p_img; m_imageView.setTag(p_str); m_prgBar = p_prgBar; ImagesQueue m_imagesQueue = new ImagesQueue(); /** * This is method to stop current running thread. */ public void stopThread() m_imageLoaderThread.interrupt(); /** * Stores list of image to be downloaded in stack. */ class ImagesQueue private Stack<ImageToLoad> m_imagesToLoad = new Stack<ImageToLoad>(); /** * Removes all instances of this ImageView * * @param p_ivImage * imageView */ public void Clean(ImageView p_ivImage) throws CustomException try for (int m_i = 0; m_i < m_imagesToLoad.size();) if (m_imagesToLoad.get(m_i).m_imageView == p_ivImage) m_imagesToLoad.remove(m_i); else m_i++; catch (Throwable t) CustomLogHandler.printErrorlog(t); throw new CustomException(TAG + " Error in Clean(ImageView p_image) of ImageLoader", t); /** * * This is class waits until there are any images to load in the queue. */ class ImagesLoader extends Thread public void run() try while (true) if (m_imagesQueue.m_imagesToLoad.size() == 0) synchronized (m_imagesQueue.m_imagesToLoad) m_imagesQueue.m_imagesToLoad.wait(); if (m_imagesQueue.m_imagesToLoad.size() != 0) ImageToLoad m_imageToLoadObj; synchronized (m_imagesQueue.m_imagesToLoad) m_imageToLoadObj = m_imagesQueue.m_imagesToLoad.pop(); Bitmap m_bmp = getBitmap(m_imageToLoadObj.m_url); m_cache.put(m_imageToLoadObj.m_url, m_bmp); if (((String) m_imageToLoadObj.m_imageView.getTag()).equals(m_imageToLoadObj.m_url)) BitmapDisplayer m_bmpdisplayer = new BitmapDisplayer(m_bmp, m_imageToLoadObj.m_imageView, m_imageToLoadObj.m_prgBar); Activity m_activity = (Activity) m_imageToLoadObj.m_imageView.getContext(); m_activity.runOnUiThread(m_bmpdisplayer); if (Thread.interrupted()) break; catch (InterruptedException e) /* * allow thread to exit */ catch (Throwable t) CustomLogHandler.printErrorlog(t); ImagesLoader m_imageLoaderThread = new ImagesLoader(); /** * This class Used to display bitmap in the UI thread */ class BitmapDisplayer implements Runnable Bitmap m_bmp; ImageView m_imageView; ProgressBar m_prgBar; public BitmapDisplayer(Bitmap p_bmp, ImageView p_imgview, ProgressBar p_prgBar) m_bmp = p_bmp; m_imageView = p_imgview; m_prgBar = p_prgBar; public void run() if (m_bmp != null) m_imageView.setImageBitmap(m_bmp); m_prgBar.setVisibility(View.GONE); m_imageView.setVisibility(View.VISIBLE);
使用上面的类如下:
首先,您需要将ProgressBar
放入您的自定义布局中,您的ImageView
如下所示:
<RelativeLayout android:layout_ android:layout_ android:id="@+id/RelativeImagelayout"> <ProgressBar android:id="@+id/Progress" android:layout_ android:layout_ android:layout_marginTop="10dp"/> <ImageView android:id="@+id/ivImage" android:layout_ android:layout_ android:layout_marginTop="10dp" android:clickable="false"/> </RelativeLayout>
在您的适配器类中创建ImageLoader
类的实例,并在您的getView
方法中使用它:
ImageView m_ibImage = (ImageView) v.findViewById(R.id.ivImage); ProgressBar m_pbProgress = (ProgressBar) v.findViewById(R.id.Progress); if (products.get(arg0).getImage().toString().equals(null) || products.get(arg0).getImage().toString().equals("")) m_pbProgress.setVisibility(View.INVISIBLE); m_ibImage.setVisibility(View.VISIBLE); else if (!products.get(arg0).getImage().toString().equals(null)) m_imgLoader.DisplayImage(products.get(arg0).getImage(), m_ibImage, m_pbProgress);
希望对你有帮助。
谢谢
【讨论】:
我不确定你是否理解我的问题。我想先在同一个自定义视图中加载 TextView,然后再加载 ImageView。延迟加载图像对我有用,我对此没有问题。谢谢。【参考方案3】:在我看来,您提到的答案不好。例如,如果您有 50 张图像,当用户向上/向下滚动整个列表时,该示例项目将产生 50 个线程。这对手机等移动设备不利。附带说明一下,他的“惰性列表”概念与 Android SDK 定义的概念不同。懒加载列表视图的示例代码,请看:
[Android SDK]/samples/android-x/ApiDemos/src/com/example/android/apis/view/List13.java
x
是 API 级别。您可以在任何模拟器中测试编译后的应用,打开应用API Demos > Views > Lists > 13. Slow Adapter。
关于您当前的方法。您不应该使用AsyncTask
来下载图像。 documentation 说:
理想情况下,AsyncTasks 应该用于短操作(最多几秒钟。)
你应该改为:
使用service 在后台下载图像。请注意,服务在主 UI 线程上运行,因此为避免使用NetworkOnMainThreadException
,您需要在服务中使用类似 Thread
的内容。
使用content provider 管理SD 卡上下载的图像。例如,您将原始 URL 映射到下载的相应文件。
与内容提供者一起,为您的网格视图使用CursorAdapter
,为承载网格视图的活动/片段使用loaders。
基本上,在用户第一次打开您的活动时,您会创建新的适配器并将其设置为网格视图。所以它与内容提供者有联系。然后您启动服务以检查和下载图像。对于下载的每个图像,您将其插入内容提供程序。提供者通知任何观察者有关更改 - 您的活动/片段(加载器)接收通知并更新 UI。
【讨论】:
以上是关于使用从互联网下载的图像延迟加载 GridView的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Android 的 ListView 中延迟加载图像