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!
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:
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.