Search code examples
javaswinglazy-loading

How to lazy load images in java swing?


I want to load multiple images into a JPanel, and I prefer to load them on demand rather than all at once, especially since I'm using JScrollPane. Most of the images I load are from direct URLs, which I store in an array of Strings. I load them one by one in a for loop using my ImageLoader method

private void loadOnlineImages(String[] images, int maxWidth, int maxHeight) {
        imagesPanel.removeAll();
        imagesPanel.setLayout(new WrapLayout(FlowLayout.LEFT));
        footerPanel.setTotalItems(images.length);

        for (String image : images) {
            ImageLoader onlineImageLoader =
                    new ImageLoader(imagesPanel, footerPanel, image, maxWidth, maxHeight);
            onlineImageLoader.loadImage();
        }

        imagesPanel.revalidate();
        imagesPanel.repaint();
    }

I tried to use imagesPanel.isDisplayable and expected it to load the images only when they're visible in the JScrollPane, but it didn't work, and all the images still load simultaneously, which freezes the application. Most of the images I load are 10-50 KBs, so when I load 20 images, it doesn't freeze, but when I load 100, it freezes.

Here is the ImageLoader class used to load the images.

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URI;
import java.util.Iterator;

public class ImageLoader {
    ProgressListener progressListener;
    JPanel footerPanel;
    JPanel imagesPanel;
    String imageUrl;
    int maxWidth;
    int maxHeight;

    public ImageLoader(JPanel imagesPanel, JPanel footerPanel, String imageUrl, int maxWidth, int maxHeight) {
        this.imagesPanel = imagesPanel;
        this.imageUrl = imageUrl;
        this.maxWidth = maxWidth;
        this.maxHeight = maxHeight;
        this.footerPanel = footerPanel;
        progressListener = new ProgressListener(footerPanel);
    }

    // Load the image only when it becomes visible
    public void loadImage() {
        new Thread(() -> {
            try {
                URI uri = URI.create(imageUrl);
                ImageInputStream imageInputStream = ImageIO.createImageInputStream(uri.toURL().openStream());
                Iterator<ImageReader> iterator = ImageIO.getImageReaders(imageInputStream);
                if (iterator.hasNext()) {
                    ImageReader reader = iterator.next();
                    reader.setInput(imageInputStream);
                    reader.addIIOReadProgressListener(progressListener);
                    BufferedImage image = reader.read(reader.getMinIndex());
                    final ImageIcon icon = new ImageIcon(image);

                    // Check if the image is still required to be shown
                    if (imagesPanel.isDisplayable()) {
                        SwingUtilities.invokeLater(() -> {
                            JLabel imageLabel = new JLabel(IconScaler.createScaledIcon(icon, maxWidth, maxHeight));
                            imagesPanel.add(imageLabel);
                            imagesPanel.revalidate();
                            imagesPanel.repaint();
                        });
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

Thank you so much in advance for your assistance!


Solution

  • Using some of the suggestions in the link provided by MadProgrammer, I modified the code found in my link from above to do lazy loading.

    The basics changes are to:

    1. initially load the ListModel with a Thumbnail object that contains the File but a null Icon
    2. create a Set to contain all the Files to be loaded
    3. add a ChangeListener to the viewport of the scroll pane
    4. when the ChangeListener is invoked I get all the indexes of the images currently visible in the viewport. I then check the File of each image to see if it needs to be loaded. If so, I invoke the ThumbnailWorker to load the image (and remove it from the set of files to be loaded).

    Here are the new classes:

    ThumbnailApp

    import java.io.*;
    import java.util.concurrent.*;
    import java.util.*;
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    
    public class ThumbnailApp
    {
        private DefaultListModel<Thumbnail> model = new DefaultListModel<Thumbnail>();
        private JList<Thumbnail> list = new JList<Thumbnail>(model);
        private Set<File> filesToBeLoaded = new HashSet<>();
        private ExecutorService service;
    
        public ThumbnailApp()
        {
            int processors = Runtime.getRuntime().availableProcessors();
            service = Executors.newFixedThreadPool( processors - 2 );
        }
    
        public JPanel createContentPane()
        {
            JPanel cp = new JPanel( new BorderLayout() );
    
            list.setCellRenderer( new ThumbnailRenderer<Thumbnail>() );
            list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
            list.setVisibleRowCount(-1);
            Icon empty = new EmptyIcon(160, 160);
            Thumbnail prototype = new Thumbnail(new File("PortugalSpain-000.JPG"), empty);
            list.setPrototypeCellValue( prototype );
    
            JScrollPane scrollPane = new JScrollPane( list );
            cp.add(scrollPane, BorderLayout.CENTER);
    
            scrollPane.getViewport().addChangeListener((e) ->
            {
                int first = list.getFirstVisibleIndex();
                int last = list.getLastVisibleIndex();
                System.out.println(first + " : " + last);
    
                if (first == -1) return;
    
                for (int i = first; i <= last; i++)
                {
                    Thumbnail thumbnail = model.elementAt(i);
                    File file = thumbnail.getFile();
    
                    if (filesToBeLoaded.contains(file))
                    {
                        filesToBeLoaded.remove(file);
                        service.submit( new ThumbnailWorker(thumbnail.getFile(), model, i) );
                    }
                }
    
                if (filesToBeLoaded.isEmpty())
                    service.shutdown();
            });
    
            return cp;
        }
    
        public void loadImages(File directory)
        {
            new Thread( () -> createThumbnails(directory) ).start();
        }
    
        private void createThumbnails(File directory)
        {
            try
            {
                File[] files = directory.listFiles((d, f) -> {return f.endsWith(".JPG");});
    
                for (File file: files)
                {
                    filesToBeLoaded.add( file );
                    Thumbnail thumbnail = new Thumbnail(file, null);
                    model.addElement( thumbnail );
                }
            }
            catch(Exception e) { e.printStackTrace(); }
        }
    
        private static void createAndShowGUI()
        {
            ThumbnailApp app = new ThumbnailApp();
    
            JFrame frame = new JFrame("ListDrop");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setContentPane( app.createContentPane() );
            frame.setSize(1600, 900);
            frame.setVisible(true);
    
    //      File directory = new File("C:/Users/netro/Pictures/TravelSun/2019_01_Cuba");
            File directory = new File("C:/Users/netro/Pictures/TravelAdventures/2018_PortugalSpain");
            app.loadImages( directory );
        }
        public static void main(String[] args)
        {
            javax.swing.SwingUtilities.invokeLater(() -> createAndShowGUI());
        }
    }
    

    Thumbnail

    import java.io.File;
    import javax.swing.Icon;
    
    public class Thumbnail
    {
        private File file;
        private Icon icon;
    
        public Thumbnail(File file, Icon icon)
        {
            this.file = file;
            this.icon = icon;
        }
    
        public Icon getIcon()
        {
            return icon;
        }
    
        public void setIcon(Icon icon)
        {
            this.icon = icon;
        }
    
        public File getFile()
        {
    //      return file.getName();
            return file;
        }
    }
    

    ThumbnailWorker

    import java.awt.*;
    import java.awt.image.*;
    import java.io.*;
    import java.util.Iterator;
    import javax.imageio.*;
    import javax.imageio.stream.*;
    import javax.swing.*;
    
    public class ThumbnailWorker extends SwingWorker<Image, Void>
    {
        private File file;
        private DefaultListModel<Thumbnail> model;
        private int index;
    
        public ThumbnailWorker(File file, DefaultListModel<Thumbnail> model, int index)
        {
            this.file = file;
            this.model = model;
            this.index = index;
        }
    
        @Override
        protected Image doInBackground() throws IOException
        {
    //      Image image = ImageIO.read( file );
    
            Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("jpg");
            ImageReader reader = readers.next();
            ImageReadParam irp = reader.getDefaultReadParam();
    //      irp.setSourceSubsampling(10, 10, 0, 0);
            irp.setSourceSubsampling(5, 5, 0, 0);
            ImageInputStream stream = new FileImageInputStream( file );
            reader.setInput(stream);
            Image image = reader.read(0, irp);
    
            int width = 160;
            int height = 90;
    
            if (image.getHeight(null) > image.getWidth(null))
            {
                width = 90;
                height = 160;
            }
    
            BufferedImage scaled = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics2D g2d = scaled.createGraphics();
            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    
            g2d.drawImage(image, 0, 0, width, height, null);
            g2d.dispose();
            image = null;
    
            return scaled;
       }
    
       @Override
       protected void done()
       {
           try
           {
               ImageIcon icon = new ImageIcon( get() );
               Thumbnail thumbnail = model.get( index );
               thumbnail.setIcon( icon );
               model.set(index, thumbnail);
    //         System.out.println("finished: " + file);
           }
           catch (Exception e)
           {
               e.printStackTrace();
           }
       }
    }
    

    ThumbnailRenderer

    import java.awt.Component;
    import javax.swing.*;
    import javax.swing.border.EmptyBorder;
    
    public class ThumbnailRenderer<E> extends JLabel implements ListCellRenderer<E>
    {
        public ThumbnailRenderer()
        {
            setOpaque(true);
            setHorizontalAlignment(CENTER);
            setVerticalAlignment(CENTER);
            setHorizontalTextPosition( JLabel.CENTER );
            setVerticalTextPosition( JLabel.BOTTOM );
            setBorder( new EmptyBorder(4, 4, 4, 4) );
        }
    
        /*
         *  Display the Thumbnail Icon and file name.
         */
        public Component getListCellRendererComponent(JList<? extends E> list, E value, int index, boolean isSelected, boolean cellHasFocus)
        {
            if (isSelected)
            {
                setBackground(list.getSelectionBackground());
                setForeground(list.getSelectionForeground());
            }
             else
            {
                setBackground(list.getBackground());
                setForeground(list.getForeground());
            }
    
            //Set the icon and filename
    
            Thumbnail thumbnail = (Thumbnail)value;
            setIcon( thumbnail.getIcon() );
            setText( thumbnail.getFile().getName() );
    
    //      System.out.println(thumbnail.getFileName());
    
            return this;
        }
    }
    

    EmptyIcon

    import java.awt.*;
    import javax.swing.*;
    
    public class EmptyIcon implements Icon
    {
        private int width;
        private int height;
    
        public EmptyIcon(int width, int height)
        {
            this.width = width;
            this.height = height;
        }
    
        public int getIconWidth()
        {
            return width;
        }
    
        public int getIconHeight()
        {
            return height;
        }
    
        public void paintIcon(Component c, Graphics g, int x, int y)
        {
        }
    }
    

    You might also want to consider removing the ChangeListener from the viewport once the filesToBeLoaded Set is empty.