What might be easier is to use a backing buffer of some kind and only paint to it when something changes.
This means that calls to paintComponent are only painting the backing buffer, making it faster, and would allow to paint the backing buffer in a second thread if the list become to large.
Updated with example

public class BackingBuffer {
public static void main(String[] args) {
new BackingBuffer();
}
public BackingBuffer() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException ex) {
} catch (InstantiationException ex) {
} catch (IllegalAccessException ex) {
} catch (UnsupportedLookAndFeelException ex) {
}
File[] imageFiles = new File("D:/hold/ScaledImages").listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
String name = pathname.getName().toLowerCase();
return name.endsWith(".png") || name.endsWith(".jpg") || name.endsWith(".gif");
}
});
ImagesPane imagesPane = new ImagesPane();
for (File file : imageFiles) {
try {
BufferedImage image = ImageIO.read(file);
imagesPane.addImage(image);
} catch (Exception e) {
e.printStackTrace();
}
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(imagesPane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class ImagesPane extends JPanel {
private BufferedImage backingBuffer;
private List<Image> images;
private Timer updateTimer;
public ImagesPane() {
images = new ArrayList<Image>(25);
updateTimer = new Timer(125, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
updateBuffer();
repaint();
}
});
updateTimer.setRepeats(false);
updateTimer.setCoalesce(true);
}
public void addImage(Image image) {
// You could devise some kind of algorithim to determine if was possible
// to image the image into the existing backing buffer or not.
// It would save having to recreate the backing buffer unless it
// really was required...
images.add(image);
invalidate();
}
public void removeImage(Image image) {
images.remove(image);
invalidate();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
protected void updateBuffer() {
if (backingBuffer == null || backingBuffer.getWidth() != getWidth() || backingBuffer.getHeight() != getHeight()) {
if (getWidth() > 0 && getHeight() > 0) {
backingBuffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
}
}
if (backingBuffer != null) {
Graphics2D g2d = backingBuffer.createGraphics();
int y = 0;
int x = 0;
int rowHeight = 0;
for (Image image : images) {
rowHeight = Math.max(image.getHeight(this), rowHeight);
if (x + image.getWidth(this) > getWidth() && x != 0) {
x = 0;
y += rowHeight;
}
g2d.drawImage(image, x, y, this);
x += image.getWidth(this);
if (x > getWidth()) {
x = 0;
y += rowHeight;
rowHeight = 0;
}
}
g2d.dispose();
}
}
@Override
public void invalidate() {
// This method can be called repeatly in quick sucession, rather then
// reacting to each call, I want to delay performing the update,
// which might be costly in time and memory until it's all settled down
// a little...
super.invalidate();
updateTimer.restart();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (backingBuffer != null) {
g.drawImage(backingBuffer, 0, 0, this);
}
}
}
}
Now, if you wanted to use a back ground thread of some sort, I'd probably use a SwingWorker
. The only thing you really would need in additional is some kind of flag you could raise so you knew that a worker was updating the buffer (as workers are non-reentrant (the same instance can't be run twice))
The worker would create a new, temporary buffer it could work, as you don't want to interfere with the one that is currently being used to paint on the screen (or you will end up with dirty paints) and once complete, you could switch the buffers in the done
method and call repaint
on the component to have it updated on the screen...
UPDATED with selection highlighting

You could highlight each image directly in the backing buffer, but I personally think this is an expensive exercise, as you would need to update the backing buffer on each click.
A better approach would be to maintain a Map
of image bounds keyed back to the individual images. When you updated the buffer, you would recreate this map.
Using this map, you could determine if any "images" where clicked. I would then place a reference of the image into a list, which I would then use when painting the component...
public class BackingBuffer {
public static void main(String[] args) {
new BackingBuffer();
}
public BackingBuffer() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException ex) {
} catch (InstantiationException ex) {
} catch (IllegalAccessException ex) {
} catch (UnsupportedLookAndFeelException ex) {
}
File[] imageFiles = new File("C:/hold/ScaledImages").listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
String name = pathname.getName().toLowerCase();
return name.endsWith(".png") || name.endsWith(".jpg") || name.endsWith(".gif");
}
});
ImagesPane imagesPane = new ImagesPane();
for (File file : imageFiles) {
try {
BufferedImage image = ImageIO.read(file);
imagesPane.addImage(image);
} catch (Exception e) {
e.printStackTrace();
}
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(imagesPane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class ImagesPane extends JPanel {
private Map<Image, Rectangle> mapBounds;
private BufferedImage backingBuffer;
private List<Image> images;
private Timer updateTimer;
private List<Image> selected;
public ImagesPane() {
images = new ArrayList<Image>(25);
selected = new ArrayList<Image>(25);
updateTimer = new Timer(125, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
updateBuffer();
repaint();
}
});
updateTimer.setRepeats(false);
updateTimer.setCoalesce(true);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (mapBounds != null) {
boolean shouldPaint = false;
for (Image image : mapBounds.keySet()) {
Rectangle bounds = mapBounds.get(image);
if (bounds.contains(e.getPoint())) {
if (selected.contains(image)) {
shouldPaint = true;
selected.remove(image);
} else {
shouldPaint = true;
selected.add(image);
}
// In it's current form, there is not overlapping, if you
// have overlapping images, you may want to reconsider this
break;
}
}
if (shouldPaint) {
repaint();
}
}
}
});
}
public void addImage(Image image) {
// You could devise some kind of algorithim to determine if was possible
// to image the image into the existing backing buffer or not.
// It would save having to recreate the backing buffer unless it
// really was required...
images.add(image);
invalidate();
}
public void removeImage(Image image) {
images.remove(image);
if (mapBounds != null) {
mapBounds.remove(image);
}
if (selected.contains(image)) {
selected.remove(image);
}
invalidate();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
protected void updateBuffer() {
if (backingBuffer == null || backingBuffer.getWidth() != getWidth() || backingBuffer.getHeight() != getHeight()) {
if (getWidth() > 0 && getHeight() > 0) {
backingBuffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
}
}
if (backingBuffer != null) {
mapBounds = new WeakHashMap<Image, Rectangle>(images.size());
Graphics2D g2d = backingBuffer.createGraphics();
int y = 0;
int x = 0;
int rowHeight = 0;
for (Image image : images) {
rowHeight = Math.max(image.getHeight(this), rowHeight);
if (x + image.getWidth(this) > getWidth() && x != 0) {
x = 0;
y += rowHeight;
}
mapBounds.put(image, new Rectangle(x, y, image.getWidth(this), image.getHeight(this)));
g2d.drawImage(image, x, y, this);
x += image.getWidth(this);
if (x > getWidth()) {
x = 0;
y += rowHeight;
rowHeight = 0;
}
}
g2d.dispose();
}
}
@Override
public void invalidate() {
// This method can be called repeatly in quick sucession, rather then
// reacting to each call, I want to delay performing the update,
// which might be costly in time and memory until it's all settled down
// a little...
super.invalidate();
updateTimer.restart();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (backingBuffer != null) {
g.drawImage(backingBuffer, 0, 0, this);
if (selected != null) {
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(UIManager.getColor("List.selectionBackground"));
for (Image image : selected) {
Rectangle bounds = mapBounds.get(image);
if (bounds != null) {
Composite composite = g2d.getComposite();
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
g2d.fill(bounds);
g2d.setComposite(composite);
g2d.draw(bounds);
}
}
}
}
}
}
}