0

I have many images that get fetched via a series of threaded HTTP network calls. I'm using Callables and Futures to manage this process. As each image comes back from the server, I want to display it on a JPanel without waiting for the other images to return.

This code works, but the UI is not updated until ALL of the images come back:

private void loadAndDisplayImages() throws InterruptedException, ExecutionException {      

    final List<Callable<Image>> partitions = new ArrayList<Callable<Image>>();

    for(final MediaFeedData data : imagesList) {
        partitions.add(new Callable<Image>() {
            public Image call() throws Exception {
                    String url = data.getImageUrl();
                    return ImageDisplayer.displayImageFromUrl(url, imageSize);
                }
            }        
        });
    }

    // for testing, use only a single thread to slow down rendering
    final ExecutorService executorPool = Executors.newFixedThreadPool(1); //numImages);    

    // run each callable, capture the results in a list of futures
    final List<Future<Image>> futureImages = 
            executorPool.invokeAll(partitions, 10000, TimeUnit.SECONDS);

    for(final Future<Image> img : futureImages) {
        Image image = img.get(); // this will block the UI

        final ImageButton imageButton = new ImageButton(image, imageSize);

        SwingUtilities.invokeLater(new Runnable(){
            @Override public void run() {
                imagesPanel.add(imageButton);
                frame.validate();
                frame.setVisible(true);
            }
        });
    }

    executorPool.shutdown();
}
AndroidDev
  • 20,466
  • 42
  • 148
  • 239
  • Consider using a `SwingWorker`, which extends from `Callable` and it's `done` method, add the image to the panel. Don't forget to call `revalidate` and `repaint` – MadProgrammer Nov 20 '14 at 03:21
  • Try to use `SwingWorker` it would help you concurrent updating the GUI. Check this link https://docs.oracle.com/javase/tutorial/uiswing/concurrency/interim.html – Vighanesh Gursale Nov 20 '14 at 03:22
  • I've spent hours trying to use it. In fact, the method that calls this function is a SwingWorker. But I can't figure out how to get it to update my UI. – AndroidDev Nov 20 '14 at 03:25

1 Answers1

2

Consider using a SwingWorker in combination with an ExecutorService...

SwingWorker...

    public class ImageLoaderWorker extends SwingWorker<Image, Image> {

        private File source;
        private JPanel container;

        public ImageLoaderWorker(File source, JPanel container) {
            this.source = source;
            this.container = container;
        }

        @Override
        protected Image doInBackground() throws Exception {
            return ImageIO.read(source);
        }

        @Override
        protected void done() {
            try {
                Image img = get();
                JLabel label = new JLabel(new ImageIcon(img));
                container.add(label);
                container.revalidate();
                container.repaint();
            } catch (InterruptedException | ExecutionException ex) {
                ex.printStackTrace();
            }
        }

    }

ExecutorService...

ExecutorService executor = Executors.newFixedThreadPool(4);
File images[] = new File("...").listFiles(new FileFilter() {
    @Override
    public boolean accept(File pathname) {
        String name = pathname.getName().toLowerCase();
        return name.endsWith(".jpg") || name.endsWith(".png");
    }
});

for (File img : images) {

    executor.submit(new ImageLoaderWorker(img, this));

}

Runnable Example...

This simply scans a directory and loads the images, but the concept is basically the same...

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileFilter;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.Scrollable;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class TestImageLoader {

    public static void main(String[] args) {
        new TestImageLoader();
    }

    public TestImageLoader() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new JScrollPane(new TestPane()));
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel implements Scrollable {

        public TestPane() {
            setLayout(new GridLayout(0, 4));
            ExecutorService executor = Executors.newFixedThreadPool(4);
            File images[] = new File("...").listFiles(new FileFilter() {
                @Override
                public boolean accept(File pathname) {
                    String name = pathname.getName().toLowerCase();
                    return name.endsWith(".jpg") || name.endsWith(".png");
                }
            });

            for (File img : images) {

                executor.submit(new ImageLoaderWorker(img, this));

            }
        }

        @Override
        public Dimension getPreferredScrollableViewportSize() {
            return new Dimension(600, 600);
        }

        @Override
        public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
            return 128;
        }

        @Override
        public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
            return 128;
        }

        @Override
        public boolean getScrollableTracksViewportWidth() {
            return false;
        }

        @Override
        public boolean getScrollableTracksViewportHeight() {
            return false;
        }

    }

    public class ImageLoaderWorker extends SwingWorker<Image, Image> {

        private File source;
        private JPanel container;

        public ImageLoaderWorker(File source, JPanel container) {
            this.source = source;
            this.container = container;
        }

        @Override
        protected Image doInBackground() throws Exception {
            return ImageIO.read(source);
        }

        @Override
        protected void done() {
            try {
                Image img = get();
                JLabel label = new JLabel(new ImageIcon(img));
                container.add(label);
                container.revalidate();
                container.repaint();
            } catch (InterruptedException | ExecutionException ex) {
                ex.printStackTrace();
            }
        }

    }

}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • It's something most people forget (or don't realise) about `SwingWorker` ;) – MadProgrammer Nov 20 '14 at 04:02
  • After looking at this, I think that it is basically doing the same thing as my code is. My ExecutorService does the http fetching with one thread per image, and is pretty fast. My problem is getting back to the UI while the ExecutorService still has open threads. I could stick a SwingWorker in the place of the SwingUtilities, but I don't see that this changes anything. In fact, I'm not even sure if I have a problem at all. If I try to fetch 500 images to stress test this, the UI takes about 1-2 seconds to totally repaint (starting from when the first image displays). – AndroidDev Nov 20 '14 at 04:26
  • I don't know if this is a clue that my code is working as I want, or if it is just that it takes time for all of these images to render on the screen. Am I getting the gist of your example? Thanks again. I really appreciate it! – AndroidDev Nov 20 '14 at 04:27
  • Look at the `done` method of the `SwingWorker`, this is called after `doInBackground` returns, but is executed within the context of the EDT – MadProgrammer Nov 20 '14 at 04:28
  • If it's a rendering issue, there might be much you can do, other then reduce the number of threads you are using and maybe put in a `Thread.yield` or `Thread.sleep` into the worker – MadProgrammer Nov 20 '14 at 04:29
  • Right, but compare that to the SwingUtilities block at the end of my loop, which runs at the end of each thread. Isn't that similar? – AndroidDev Nov 20 '14 at 04:30
  • 1
    Yes, similar, the SwingWorker just makes it easier ;) – MadProgrammer Nov 20 '14 at 04:30
  • If it is a rendering issue, I'm fine with that. If my threads are coming in so fast that rendering is the only bottleneck, then mission accomplished. I'm just trying to deduce whether my UI is updating with each thread or just in one batch at the end. – AndroidDev Nov 20 '14 at 04:31
  • Yeah, the EDT might be chocking. You could use `invokeAndWait`, but I'm not sure you'd get much more difference – MadProgrammer Nov 20 '14 at 04:33