Java Swing: JList with ListCellRenderer selected item of different heights

I am creating a custom ListCellRenderer. I know that you can have different sizes for each individual cell. But now I want to have a different dimension for the selected cell. One way or another, JList caches the dimension for each individual cell when it first calculates the boundaries for each cell. This is my code:

public class Test { static class Oh extends JPanel { public Oh() { setPreferredSize(new Dimension(100, 20)); } protected void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(Color.WHITE); g.fillRect(0, 0, getWidth(), getHeight()); } } static class Yeah extends JPanel { private boolean isSelected; public Yeah(boolean isSelected) { setPreferredSize(new Dimension(100, 100)); this.isSelected = isSelected; } protected void paintComponent(Graphics g) { super.paintComponent(g); //setSize(100, 100); // doesn't change the bounds of the component //setBounds(0, 0, 100, 100); // this doesn't do any good either. if (isSelected) g.setColor(Color.GREEN); else g.setColor(Color.BLACK); g.fillRect(0, 0, getWidth(), getHeight()); } } public static void main(String[] args) { JFrame f = new JFrame(); f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); f.setSize(800, 500); Vector<Integer> ints = new Vector<Integer>(); for (int i = 0; i < 100; i++) { ints.add(i); } JList list = new JList(ints); list.setCellRenderer(new ListCellRenderer() { public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { if (isSelected || ((Integer) value) == 42) return new Yeah(isSelected); else return new Oh(); } }); //list.setPrototypeCellValue(null); //list.setFixedCellHeight(-1); f.add(new JScrollPane(list)); f.setVisible(true); } } 

In the comments you can see what I have already tried.

I searched and found a lot of useless articles for a long time, some of them relate to the ListCellRenderer / dynamic height thing, but they only work because the height remains the same for individual cells. My heights are changing, so how do I do this?

+4
source share
7 answers

Thanks to Rastislav Komara, I was able to solve this quite easily:

I created an inner class that extends BasicListUI and created a public method that is called in ListSelectionListener.valueChanged:

 private class MyRenderer implements ListCellRenderer { public int listSelectedIndex = -1; public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { if (index == listSelectedIndex) return new Yeah(isSelected); else return new Oh(); } } MyRenderer lcr = new MyRenderer(); private class MyListUI extends BasicListUI { public void triggerUpdate() { lcr.listSelectedIndex = list.getSelectedIndex(); updateLayoutState(); list.revalidate(); } } 

The updateLayoutState method usually starts when the JList heights change. The only β€œcrazy” thing I'm doing here is that my rendering should know what the selected index is. This is because the updateLayoutState method does not use the selected index in height calculations. Somehow using list.getSelectedIndex () inside getListCellRendererComponent does not work.

Edit:
Check also anser for nevers and cleopatra, they look smarter, first try ...

+1
source

Basically, there are two aspects to the problem, both of which are located in the delegate ui

  • he cannot adjust the rendering to its real state during measurement, that is, completely ignores the choice (and focus).
  • It is often stubborn from being forced to recalculate the sizes of cached cells: it does not have a public api to do this and only voluntarily change the model.

The first remedy is really a visualization tool: implement to ignore the selected flag and request a list for real selection, as described by @Andy. In code using OP components

 ListCellRenderer renderer = new ListCellRenderer() { Yeah yeah = new Yeah(false); Oh oh = new Oh(); @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { // ignore the given selection index, query the list instead if (list != null) { isSelected = list.isSelectedIndex(index); } if (isSelected || ((Integer) value) == 42) { yeah.isSelected = isSelected; return yeah; } return oh; } }; list.setCellRenderer(renderer); 

To fix the second, a user delegate ui might be selected (as suggested in other answers). Although some work in the general case, if support for multiple LAFs is needed.

A less intrusive but slightly dirty method to force ui to voluntarily update its cache is to send a fake ListDataEvent to selectionChange:

 ListSelectionListener l = new ListSelectionListener() { ListDataEvent fake = new ListDataEvent(list, ListDataEvent.CONTENTS_CHANGED, -1, -1); @Override public void valueChanged(ListSelectionEvent e) { JList list = (JList) e.getSource(); ListDataListener[] listeners = ((AbstractListModel) list.getModel()) .getListDataListeners(); for (ListDataListener l : listeners) { if (l.getClass().getName().contains("ListUI")) { l.contentsChanged(fake); break; } } } }; list.addListSelectionListener(l); 

BTW, the JXList of the SwingX project has a user ui delegate - mainly to support sorting / filtering - using the public api to recount the cache, then the ListSelectionListener above will be simplified (and cleared :-) to

  ListSelectionListener l = new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { ((JXList) e.getSource()).invalidateCellSizeCache(); } }; list.addListSelectionListener(l); 
+8
source

I just implemented this feature. The problem is that the cell renderer is requested twice to render the cell. In the first round, all entries in the list are displayed without selection, then the selected cells are displayed again using the selection. Therefore, if you provide a preferred size in the first round, it is cached and also used for the second round.

The trick is to ignore the boolean parameter isSelected in getListCellRendererComponent and determine the selection state by checking if list.getSelectedIndices() contains the given index.

But I still have a problem that after the list becomes visible, the height of the displayed components is sometimes large / small. After resizing the list with the mouse, everything will be fine. I played with checking / revalidate, repaint, reset of cached heights, but nothing worked. Swinging is sometimes a little weird ...

+5
source

JList cannot resize cells depending on selection or anything else. The list uses "cached" sizes. If there is a new cellRenderer, provided that these sizes are recalculated and applied in all cells in the list. I think the reason is the performance for a list with a lot of entries. A possible solution is to write your own ListUI implementation, which can use different sizes for selected and unselected cells. It also makes it possible to adjust the size of the cells around the selection by logarithm or other interpolation. Hope you have a great reason for this. This is a great job!

+4
source

I was tearing my hair over this stupid JList line height issue. I have a cell renderer that sets a variable row height for each row - the problem is that the JList stores the heights cache.

Using other answers, I think I hit the holy grail. Here he is:

Use the simplified version of BasicListUI created by Jaap:

 public class BetterListUI extends BasicListUI { public void triggerUpdate() { updateLayoutState(); } } 

Then, when you create a JList, expand it like this:

 betterListUI = new BetterListUI(); myJList = new JList() { @Override public void repaint(long tm, int x, int y, int width, int height) { betterListUI.triggerUpdate(); super.repaint(tm, x, y, width, height); } }; myJList.setUI(betterListUI); 

You may need to set up protection around triggerUpdate during creation, depending on your circumstances.

+2
source

JList probably caches your cell handler. Try connecting the ListSelectionListener and reinstall the renderer when you change the selection.

 ... addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent event) { if(event.getValueIsAdjusting() == false) { list.setCellRenderer(new MyRenderer()); } } ) ... 
0
source

this is a simple solution:

 public class VariableHeightListUI extends BasicListUI { @Override public void paint(Graphics g, JComponent c) { updateLayoutState(); super.paint(g, c); } } 

ListCellRenderer course, you need to write your own implementation of ListCellRenderer , and in accordance with the different state of the choice of the list item, you can set a different preferred height of the returned component.

Only one problem needs to be continued: when you select an item from the FIRST time list, you are not drawing correctly. but after that everything works well.

Hope this can help you.

0
source

All Articles