【问题标题】:Lazy loading GridView with images downloading from internet使用从互联网下载的图像延迟加载 GridView
【发布时间】:2013-03-14 11:56:18
【问题描述】:

多年来我一直在访问 Stack Overflow,这是我第一次找不到任何可以解决我问题的帖子(至少我没有看到任何帖子)。

我有一个带有自定义适配器的GridView,我已将其覆盖以返回由ImageViewTextView 创建的自定义视图。

我从带有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。它工作正常,但问题是它再次加载了整个视图。我想要做的是启动活动,首先加载标题,然后在完成下载后加载图像。使用这里的代码,它只是延迟加载网格视图的每个单元格包含的整个视图。我赢得了几秒钟,因为我没有像以前那样一次下载所有图像,但它仍然不是我要搜索的。

非常感谢。

【问题讨论】:

    标签: android android-custom-view android-gridview android-lazyloading


    【解决方案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_width="wrap_content"
        android:layout_height="wrap_content" 
        android:id="@+id/RelativeImagelayout"> 
          <ProgressBar android:id="@+id/Progress"
                    android:layout_height="wrap_content"
                    android:layout_width="wrap_content"
                    android:layout_marginTop="10dp"/>
        <ImageView
            android:id="@+id/ivImage"
            android:layout_width="80dp"
            android:layout_height="90dp"
            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。

    【讨论】:

      猜你喜欢
      • 2012-10-27
      • 2014-01-07
      • 2012-09-15
      • 2020-05-25
      • 2018-01-28
      • 2013-02-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多