Let's start with some obvious issues
- Users can (and will) scroll through the list using the keyboard as well as the mouse, so, a
MouseWheelListener
(or any sort of mouse based listener) is a bad place to start
- Swing is single threaded and not thread safe, this means that you shouldn't be performing long running or blocking operations within the context of the Event Dispatching Thread and you shouldn't be updating the UI or anything the UI relies on from outside of the Event Dispatching Thread context. See Concurrency in Swing for more details
So, what's the answer?
Well, for me, I started with the veriticalScrollBar
of the JScrollPane
and added an AdjustmentListener
to it. This will notify me when the vertical scroll bar position changes. This might not be the "best" option, but it was the obvious one to me. This means we're not relient on the monitoring the keyboard and mouse, instead, we're monitoring the state (or part of the state) of the scroll pane itself.
Next, I set up a "trigger" which was, in my test, 5 rows less then a full page of data. That is, in my example, a page was 20 rows, so my trigger was always 5 rows less (15/35/55/75/95), this means that we're attempting to load a new page slightly early.
When the AdjustmentListener
was triggered, I checked the JList#getLastVisibleIndex
property and if the values mathched, I then used a SwingWorker
to off loading the loading of the data to a different thread, but which allows me an easy way to re-sync that data back to the EDT. See Worker Threads and SwingWorker for more details
This idea might look something like...
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingWorker;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
DefaultListModel<String> model = new DefaultListModel<>();
for (int index = 0; index < 20; index++) {
model.addElement(Integer.toString(index));
}
JList<String> list = new JList<>(model);
list.setVisibleRowCount(10);
JScrollPane scrollPane = new JScrollPane(list);
scrollPane.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {
private int triggerRow = 15;
@Override
public void adjustmentValueChanged(AdjustmentEvent e) {
int lastRow = list.getLastVisibleIndex();
if (lastRow == triggerRow) {
int startingValue = model.getSize();
triggerRow += 20;
SwingWorker<List<String>, List<String>> worker = new SwingWorker<>() {
@Override
protected List<String> doInBackground() throws Exception {
List<String> values = new ArrayList<>(20);
for (int index = 0; index < 20; index++) {
values.add(Integer.toString(startingValue + index));
}
// Artificial 7 second delay...
Thread.sleep(7000);
publish(values);
return values;
}
@Override
protected void process(List<List<String>> chunks) {
for (List<String> values : chunks) {
model.addAll(values);
}
}
};
worker.execute();
}
}
});
setLayout(new BorderLayout());
add(scrollPane);
}
}
}
Now, please, understand. This is simplified solution. I've deliberately set the setVisibleRowCount
to a size less then my trigger row.
You might be able to change it so that if the last row is >=
to the trigger row, you would start loading more pages, but, you'd probably need some way to determine if a loading is in progress first and possibly modify the logic which determines the next trigger point so that it's more efficient in a more dynamic environment.
I'd also consider rolling a lot of this up into some kind of "pagination" workflow, so maybe a dedicated class you could pass the scroll pane and list into and have it manage all the core workflow and maybe trigger an observer to load the data, but that's me.
Oh, and might also consider making use of JLayer
to provide some kind of visual feedback, see How to Decorate Components with the JLayer Class for more details