more efficiently load images for JList, presumably with SwingWorker

user9851015 :

I have a problem in which I hope SwingWorker can help me, but I am not quite sure how to integrate it in my program.

The problem:

In a CardLayout I have a button on Card1 that opens Card2. Card2 has a JList with a custom renderer(extending JLabel) which will display on average 1 to 6 images which are:

  • PNGs
  • around 500kb in size
  • loaded via imageIO with the change of cards

the renderer applies heavy operations such as image scaling or blurring and than sets the image as JLabel icon.

This can almost take up to a second if around 6 images have to be rendered, which is does not happen frequently but even that occasional split second of unresponsiveness feels bad.

Now I thought a SwingWorker might help here, but I'm thoroughly confused as to how I would have to integrate it.

Assuming we had this Code snippet

import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class Example {

    private JPanel mainPanel = new JPanel();
    private JList<Product> list = new JList();
    private JScrollPane scroll = new JScrollPane();
    private Map<String, Color> colorMap = new HashMap<>();

    public Example() {
        colorMap.put("red", Color.red);
        colorMap.put("blue", Color.blue);
        colorMap.put("cyan", Color.cyan);
        colorMap.put("green", Color.green);
        colorMap.put("yellow", Color.yellow);

        mainPanel.setBackground(new Color(129, 133, 142));

        scroll.setViewportView(list);
        scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        scroll.setPreferredSize(new Dimension(80,200));

        list.setCellRenderer(new CustomRenderer());
        DefaultListModel model = new DefaultListModel();

        model.addElement(new Product("red"));
        model.addElement(new Product("yellow"));
        model.addElement(new Product("blue"));
        model.addElement(new Product("red"));
        model.addElement(new Product("cyan"));
        list.setModel(model);

        mainPanel.add(scroll);
    }

    public static void main(String[] args) throws IOException {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame("WorkerTest");
                frame.setContentPane(new Example().mainPanel);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLocation(300, 300);
                frame.setMinimumSize(new Dimension(160, 255));
                frame.setVisible(true);
            }
        });
    }

    class CustomRenderer extends JLabel implements ListCellRenderer<Product> {

        private  Product product;
        public CustomRenderer() {
            setOpaque(false);
        }

        @Override
        public Component getListCellRendererComponent(JList<? extends Product> list, Product product, int index, boolean isSelected, boolean cellHasFocus) {
            this.product = product;

            /**
             * in the actual code image is png with alpha channel respectively named to the productID of the JList object
             *
             * String id = product.getId();
             * image = ImageIO.read(getClass().getResource("../../resources/images/" + id + ".png"));
             */

            BufferedImage image1 = new BufferedImage(80, 50, BufferedImage.TYPE_INT_RGB);
            BufferedImage image2 = new BufferedImage( 80, 75, BufferedImage.TYPE_INT_RGB);
            Graphics g = image2.getGraphics();

            /**
             * this is only an example, in the actual code I might also apply gaussian blurs or rescale several time
             */
            g.drawImage(image1,0,0,null);

            setIcon(new ImageIcon(image2));

            return this;
        }

        public void paintComponent(Graphics g) {
            super.paintComponent(g);

            g.setColor(colorMap.get(product.getColor()));
            g.fillRect(0,0,80,75);
        }
    }

    class Product {

        String productID;
        String color;

        public Product(String color) {
            this.color = color;
        }

        public String getColor() {
            return color;
        }

        public String getProductID() {
            return productID;
        }
    }
}

would I have to call a SwingWorker from every getListCellRendererComponent call to take over the image operations ?

Is SwingWorker even the right tool for this problem?

any help as to how I can make this part of my GUI faster would be greatly appreciated.

EDIT: Hovercraft Full Of Eels mentioned that preloading the images could help and that loading the images from the renderer is fundamentally wrong.

This leads me to another Question:

I have a list(let's call it list1) with around 3000 objects each object has a 8kb jpg thumbnail which is load via object ID (also during the rendering) The list displays around 6 to 12 of these thumbnail at the same time (due to the List's Dimension)

when the user selects an object he can press a button to display Card2 from the Cardlayout mentioned in the original question and it's list(list2) with the Object and all it's related Object in non thumbnail view (500kb png + heavy image operation). Now I think it would be feasible to preload the non thumbnail image of the Object and it's relations selected in the first list which would be around 1-6 images. If I understood correctly what Hovercraft Full Of Eels said, then I could use a SwingWorker to load these Images after the selection of an Object from list1.

But what about the around 3000 images from list1, the program seemingly is not slowed down or becomes unresponsive because they are rather small in size and there are no heavy operations on the thumbnails, but they are still load form the list1's renderer. Would it make sense to preload the several thousand thumbnails ?

btw. feel free to tell me if this kind of question edit is not wished for and if it should be made into a question of itself.

Marco13 :

One approach might be the following:

Whenever a cell renderer component for a certain element (Product) is requested, you check whether the matching image is already loaded. If not, you start a Swing worker that does the work of loading and processing the image in the background. When the worker is done, the image is placed into a cache for later lookup. In the meantime, you let the renderer just say "Loading..." or something.

A very quick implementation is here:

enter image description here

And as an MCVE:

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.SwingWorker;

public class LazyImageLoadingCellRendererTest
{

    private JPanel mainPanel = new JPanel();
    private JList<Product> list = new JList<Product>();
    private JScrollPane scroll = new JScrollPane();

    public LazyImageLoadingCellRendererTest()
    {
        mainPanel.setBackground(new Color(129, 133, 142));

        scroll.setViewportView(list);
        scroll.setHorizontalScrollBarPolicy(
            JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        scroll.setPreferredSize(new Dimension(80, 200));

        list.setCellRenderer(new LazyImageLoadingCellRenderer<Product>(list,
            LazyImageLoadingCellRendererTest::loadAndProcessImage));
        DefaultListModel<Product> model = new DefaultListModel<Product>();

        for (int i=0; i<1000; i++)
        {
            model.addElement(new Product("id" + i));
        }
        list.setModel(model);

        mainPanel.add(scroll);
    }

    public static void main(String[] args) throws IOException
    {

        EventQueue.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                JFrame frame = new JFrame("WorkerTest");
                frame.setContentPane(
                    new LazyImageLoadingCellRendererTest().mainPanel);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLocation(300, 300);
                frame.setMinimumSize(new Dimension(160, 255));
                frame.setVisible(true);
            }
        });
    }

    private static final Random random = new Random(0);

    private static BufferedImage loadAndProcessImage(Product product)
    {
        String id = product.getProductID();
        int w = 100;
        int h = 20;
        BufferedImage image =
            new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.GREEN);
        g.fillRect(0, 0, w, h);
        g.setColor(Color.BLACK);
        g.drawString(id, 10, 16);
        g.dispose();

        long delay = 500 + random.nextInt(3000);
        try
        {
            System.out.println("Load time of " + delay + " ms for " + id);
            Thread.sleep(delay);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        return image;
    }

    class Product
    {
        String productID;

        public Product(String productID)
        {
            this.productID = productID;
        }

        public String getProductID()
        {
            return productID;
        }
    }

}

class LazyImageLoadingCellRenderer<T> extends JLabel
    implements ListCellRenderer<T>
{
    private final JList<?> owner;
    private final Function<? super T, ? extends BufferedImage> imageLookup;
    private final Set<T> pendingImages;
    private final Map<T, BufferedImage> loadedImages;

    public LazyImageLoadingCellRenderer(JList<?> owner,
        Function<? super T, ? extends BufferedImage> imageLookup)
    {
        this.owner = Objects.requireNonNull(
            owner, "The owner may not be null");
        this.imageLookup = Objects.requireNonNull(imageLookup,
            "The imageLookup may not be null");
        this.loadedImages = new ConcurrentHashMap<T, BufferedImage>();
        this.pendingImages =
            Collections.newSetFromMap(new ConcurrentHashMap<T, Boolean>());
        setOpaque(false);
    }

    class ImageLoadingWorker extends SwingWorker<BufferedImage, Void>
    {
        private final T element;

        ImageLoadingWorker(T element)
        {
            this.element = element;
            pendingImages.add(element);
        }

        @Override
        protected BufferedImage doInBackground() throws Exception
        {
            try
            {
                BufferedImage image = imageLookup.apply(element);
                loadedImages.put(element, image);
                pendingImages.remove(element);
                return image;
            }
            catch (Exception e)
            {
                e.printStackTrace();
                return null;
            }
        }

        @Override
        protected void done()
        {
            owner.repaint();
        }
    }

    @Override
    public Component getListCellRendererComponent(JList<? extends T> list,
        T value, int index, boolean isSelected, boolean cellHasFocus)
    {
        BufferedImage image = loadedImages.get(value);
        if (image == null)
        {
            if (!pendingImages.contains(value))
            {
                //System.out.println("Execute for " + value);
                ImageLoadingWorker worker = new ImageLoadingWorker(value);
                worker.execute();
            }
            setText("Loading...");
            setIcon(null);
        }
        else
        {
            setText(null);
            setIcon(new ImageIcon(image));
        }
        return this;
    }
}

Note:

This is really just a quick example showing the general approach. Of course, this could be improved in many ways. Although the actual loading process is already pulled out into a Function (thus making it generically applicable for any sort of image, regardless of where it comes from), one major caveat is that: It will try to load all images. A nice extension would be to add some smartness here, and make sure that it only loads the images for which the cells are currently visible. For example, when you have a list of 1000 elements, and want to see the last 10 elements, then you should not have to wait for 990 elements to be loaded. The last elements should be priorized higher and loaded first. However, for this, a slightly larger infrastructure (mainly: an own task queue and some stronger connection to the list and its scroll pane) may be necessary. (I'll possibly tackle this one day, because it might be a nice and interesting thing to have, but until then, the example above might do it...)

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=87881&siteId=1