Saturday, January 16, 2010

Dynamic size cell list renderer

What I mean by dynamic size is that the renderer can changed size for a given index.

Let’s take what can be seen at first as a very easy example:
For example when you select an item, you would like to increase the font size of the label.

It seems easy, let’s just create a custom renderer and change the font size based on the isSelected parameter.


public Component getListCellRendererComponent(JList list, Object value,int index, boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (isSelected) {
label.setFont(label.getFont().deriveFont(35f));
} else {
label.setFont(label.getFont().deriveFont(10f));
}
return label;
}




The font size is bigger but the size of the label is not increased, hence the label is partly hidden .

So let’s try to change the size of the label:


public Component getListCellRendererComponent(JList list, Object value,int index, boolean isSelected, boolean cellHasFocus) {

JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (isSelected) {
label.setFont(label.getFont().deriveFont(35f));
label.setPreferredSize(new Dimension(50, 50));
} else {
label.setFont(label.getFont().deriveFont(10f));
label.setPreferredSize(new Dimension(50, 15));
}
return label;
}



To our surprise it doesn’t work. Why?

The answer is in the BasicListUI class, the height of each cell is cached, it doesn’t get computed each time the getListCellRendererComponent method get called. That’s where our problem lies.

By looking a bit more closely at BasicListUI, one can see that the cache is populated in the updateLayoutState method and that’s only getting called after a change in the model.

We have to call this method in order to compute the new size of each cell, but this method is protected,so we won’t be able to call this method directly so we will use reflection.


public static void computeListSize(final JList list) {
if (list.getUI() instanceof BasicListUI) {
BasicListUI ui = (BasicListUI) list.getUI();

try {
Method method = BasicListUI.class.getDeclaredMethod("updateLayoutState");
method.setAccessible(true);
method.invoke(ui);
list.revalidate();
list.repaint();
} catch (Exception e) {
e.printStackTrace();
}
}
}




In our case we need to compute the cell size when a cell gets selected:

list.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(final ListSelectionEvent e) {
JlistUtils.computeListSize(list);
}
});



But as you can see if you run this webstart demo , it doesn’t work, so why ?
The answer is in the updateLayoutState method.:
protected void updateLayoutState(){
//…
Component c = renderer.getListCellRendererComponent(list, value, index, false, false);
//…
}



The 2 last parameters are isSelected and cellHasFocus, which means we can’t use isSelected+cellHasFocus to determine the size of the renderer.

So the last version of our renderer is:


public Component getListCellRendererComponent(JList list,Object value, int index, boolean isSelected, boolean cellHasFocus) {

JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (list.getSelectedIndex() == index) {
label.setFont(label.getFont().deriveFont(35f));
label.setPreferredSize(new Dimension(50, 50));
} else{
label.setFont(label.getFont().deriveFont(10f));
label.setPreferredSize(new Dimension(50, 15));
}
return label;
}



This time we actually have what we expected: the cell selected is bigger.






You can find the source of those demo in the following package in the source code repository here


all the jars files has been signed to run via webstart; that was needed because of the use of Method.setAccessible to be able to call a protected method

No comments:

Post a Comment