Lazy loading gallery with Java Swing

From web pages this is a well known issue: You have an image gallery with many rows of pictures. Only part of them are visible in the browser. So the first images that should be loaded are the ones that are visible. With the browser technology this is not a big deal, as each resource (image) is loaded in a separate request, so the images at the top are requested first and you can start looking at the images and, when you scroll down the ones below will be loaded already.

With a desktop application this is a bit different, as you have to do something to achieve reasonable loading times. With a desktop application the the user interface is completely defined before displaying. Imagine you have loads of images that are to be loaded: The loading from your drive takes time and consumes memory. The first issue can be resolved by loading the images asynchronously, so the UI is not blocked while the images are loaded. However you might also save memory by only keep the images that are displayed in memory. This article shows how I achieved this:

The example application shown on the side, has 30 images, in a table layout (GridLayout) with three columns each, which results in 10 rows, to display all images. The image to be displayed is the index number of the image. What cannot be seen is that the black square is the place holder. In the example the images are added to a JLabel as an icon. each label is initialized with a black square as place holder. when the image becomes visible, the icon is updated.

The main application is pretty straight forward:

<pre>public class ScrollPane extends JFrame {

private GalleryPane contentPane;

/**
* Launch the application.
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
ScrollPane frame = new ScrollPane();
frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}

/**
* Create the frame.
*/
public ScrollPane() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(100, 100, 450, 300);
createCompontents();
}

private void createCompontents() {
contentPane = new GalleryPane();
this.addComponentListener(contentPane); // resize listener to the frame
JScrollPane scrollPane = new JScrollPane(contentPane);
JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar();
BoundedRangeModel brm = verticalScrollBar.getModel();
contentPane.setBrm(brm);
verticalScrollBar.addAdjustmentListener(contentPane);
setContentPane(scrollPane);
}
}</pre>

The only notable part is in the <code>createCompontents</code>, where the model of the vertical scroll bar is extracted and set on the GalleryPane. This is the component that does all the work. There are some issues that must be considered when implementing:

<h3>Resizing</h3>

As only the images that are displayed are loaded, we must keep track on the visible part. In the context of a scrollable pane this is the viewport. When resizing the window, this portion changes size as well. Therefore we must update when the size changes. To achieve that, the GalleryPane implements the ComponentListener interface, which notifies if:

  • The component becomes hidden: this event is ignored
  • The component becomes visible: this event is ignored
  • The component is moved: this event is ignored
  • The component is resized: We recalculate the the height of the viewport and update the view, to ensure, that the images that should be visible are loaded.

<pre>    /**
* When the component is resized, the visible portion changes, take kare of the
* appropriate actions.
* @see ComponentListener#componentResized(ComponentEvent)
*/
public void componentResized(ComponentEvent e) {
int rowHeigth = SPACEING+IMAGE_DIM;
visibleNbRows = 1.0*getParent().getBounds().height/rowHeigth;
updateView(); // make sure the view is properly updated
}</pre>

This method is also called, when the component is packed.

<h3>Update when scrolling</h3>

When scrolling the visible portion changes, so we have to keep track of what is visible and when we need to load the next couple images. This is also the location where we would handle the unloading of images to save memory. The GalleryPane implements the interface AdjustmentListener, that is added to the scrollable pane. Through this interface we get notified when scrolling. The event object contains the maximum, the extent and the value. In this example I use the same information directly from the scroll bar model. This avoids handling various cases like the horizontal scrollbar was used.

<h3>Computing the correct location</h3>

This is the tricky part. We know the height of the viewport respectively the number of visible rows  (<code>visibleNbRows</code>). The first thing wee need to know is the index of the first row that is still visible:

<pre>SPACEING  + x*(SPACEING+IMAGE_DIM) = pos</pre>

This gives us the index x. pos is the position in the whole pane. The computation of this position is a bit tricky.

<pre> double percentage = brm.getValue()/(brm.getMaximum()-brm.getExtent());</pre>

This gives us the percentage of what is scrolled. However we must consider, that 0 % is at the top and 100% is at the bottom, but when scrolled to the bottom, the top position of the visible portion is not at the bottom. Therefore we must correct the top position:

<pre>    /**
* Compute the top position in the panel that is visible.
* @param percentage
* @return
*/
private double computeTopPosition(double percentage) {
/*
* The percentage indicates the scroll position, so 0% means all to
* the top, 100% is all the bottom. The top position of the bottom however
* is the height of the panel – the height of the viewport (parent). anything
* in between is procentual
*/
double top = getBounds().height*percentage;
double correction = getParent().getBounds().height*percentage;
return top-correction;
}</pre>

The last row index is computed like the first row, with the difference, that we add the <code>visibleNbRows</code>.

The updating of the images now is easy; iterate over the images that lie between the first and last row and update the label.

The code for this example can be downloaded: LazyLoadingGallery.zip

Schreibe einen Kommentar