Java JList + JScrollPane + eigener ListCellRenderer = Flackerndes Scrolling

Killkrog

Lt. Junior Grade
Registriert
Jan. 2006
Beiträge
354
Hi liebe CB'ler!

Ich habe hier ein etwas vertracktes Problem und komme nun schon seit einiger Zeit zu keiner Lösung. Vielleicht kann mir einer von euch helfen.

Folgende Situation:
Ich habe eine stinknormale JList, die ich in ein JScrollPane gepackt habe.
Die JList hat außerdem einen eigenen CellListRenderer spendiert bekommen. Dieser gibt jeweils das Listenelement selbst zurück, um das Beispiel schlank und übersichtlich zu halten.

Auftretendes Symptom:
Wenn man die Pfeiltasten Hoch und Runter auf der Tastatur verwendet, um in der Liste nach oben oder unten zu navigieren, fängt er ja irgendwann unweigerlich automatisch das Scrollen an. Hierbei "flackern" sporadisch die ersten ein bis zwei Zellen jeweils an der Kante der jeweiligen Bewegungsrichtung. Das sieht ungefähr so aus wie der Effekt, den man bekommt, wenn man sich früher bei AWT nicht ums Double-Buffering gekümmert hat.

Frage:
Habt ihr die selben Symptome? (Vielleicht liegt es ja nur an meiner Maschine)
Habt ihr eine Lösung für das Problem?

Hier noch einmal der verwendete Code:

Example.java (beinhaltet main()-Methode)
Code:
public class Example {

	// ===================================================================
	// {[> Initializers and Constructors
	// =================================
	public Example() {
		MyListCell[] cells = new MyListCell[100];
		for (int i = 0; i < cells.length; i++) {
			cells[i] = new MyListCell();
		}

		JFrame frame = new JFrame("Flickering list");
		frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
		frame.setResizable(false);

		JList list = new JList(cells);
		list.setCellRenderer(new MyListCellRenderer());
		list.setVisibleRowCount(3);

		JScrollPane sPane = new JScrollPane(list);
		sPane.setViewportBorder(new EmptyBorder(2, 2, 2, 2));

		frame.add(sPane);

		frame.pack();
		frame.setVisible(true);
	}



	// ===================================================================
	// {[> Static Methods (Public)
	// ===========================
	public static void main(String[] args) {
		new Example();
	}
}

MyListCell.java
Code:
public class MyListCell extends Component {

	// ===================================================================
	// {[> Attributes
	// ==============
	private static final int prefWidth = 550, prefHeight = 75;
	private static final Dimension prefSize = new Dimension(prefWidth, prefHeight);
	private static final BufferedImage[] images = new BufferedImage[2];

	private BufferedImage image;



	// ===================================================================
	// {[> Initializers and Constructors
	// =================================
	static {
		images[0] = new BufferedImage(prefWidth, prefHeight, BufferedImage.TYPE_INT_RGB);
		images[1] = new BufferedImage(prefWidth, prefHeight, BufferedImage.TYPE_INT_RGB);
		fillBackgroundImage(images[0], new Color(202, 202, 202), new Color(94, 94, 94));
		fillBackgroundImage(images[1], new Color(202, 114, 114), new Color(94, 53, 53));
	}



	// ===================================================================
	// {[> Static Methods (Private)
	// ============================
	private static void fillBackgroundImage(BufferedImage img, Color c1, Color c2) {
		Graphics2D g = img.createGraphics();

		g.setColor(Color.WHITE);
		g.fillRect(0, 0, prefWidth, prefHeight);

		GradientPaint gp = new GradientPaint(0, 0, c1, 0, prefHeight - 1, c2);
		g.setPaint(gp);
		g.fillRect(2, 2, prefWidth - 4, prefHeight - 4);

		g.setColor(Color.BLACK);
		g.drawRect(2, 2, prefWidth - 5, prefHeight - 5);

		g.dispose();
	}



	// ===================================================================
	// {[> Methods (Public)
	// ====================
	public Dimension getPreferredSize() {
		return prefSize;
	}

	public void paint(Graphics g) {
		g.drawImage(image, 0, 0, null);
	}

	public void setSelected(boolean selected) {
		image = selected ? images[1] : images[0];
	}
}

MyListCellRenderer.java
Code:
public class MyListCellRenderer implements ListCellRenderer {

	// ===================================================================
	// {[> Methods (Public)
	// ====================
	public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
		MyListCell cell = (MyListCell) value;
		cell.setSelected(isSelected);
		return cell;
	}
}

Vielen Dank schon im Voraus für eure Hilfe und eine gute Nacht!
Killkrog
 
Bei mir flackert nichts (Ubuntu 11.04, Intel HD 3000, aktuelles Oracle JDK). Die Hardware bzw. Software mag sicher eine Rolle spielen.

Aber Möglichkeiten zur Optimierung gibt es. Schau Dir mal den Code von DefaultListCellRenderer an. Dort hat es Hinweise zu Verbesserungsmöglichkeiten.

Warum leitest Du von java.awt.Component ab? Schon mal javax.swing.JComponent versucht?
 
Die Javadoc sagt explizit, dass man von Component ableiten soll. Natürlich habe ich JComponent trotzdem schon ausprobiert ;)
Den DefaultCellListRenderer schau ich mir mal an. Für weitere Vorschläfe bin ich trotzdem dankbar!
 
Killkrog schrieb:
Die Javadoc sagt explizit, dass man von Component ableiten soll

Wo steht das denn? Darüber bin ich noch nie gestolpert!

Die meisten CellRenderer, die ich kenne, leiten sich von konkreten Widget-Klassen ab, z.B. JLabel (so auch DefaultListCellRenderer). Deine Implementierung ist insofern eher ungewöhnlich. Natürlich ist auch hier java.awt.Component involviert, aber wird die Cell-Klasse so nicht als Heavyweight-Widget behandelt?

Ich würde versuchsweise mal von DefaultListCellRenderer ableiten.
 
soares schrieb:
Wo steht das denn? Darüber bin ich noch nie gestolpert!
http://download.oracle.com/javase/6/docs/api/javax/swing/JList.html#renderer

soares schrieb:
Die meisten CellRenderer, die ich kenne, leiten sich von konkreten Widget-Klassen ab, z.B. JLabel (so auch DefaultListCellRenderer). Deine Implementierung ist insofern eher merkwürdig.
Das ist wohl richtig und wäre in meinem Beispiel auch ungewöhnlich. In meiner Anwendung jedoch, um die es letzendlich geht, sind einige Teile der paint()-Methode sehr rechenaufwendig, aber nur einmalig notwendig. Daher habe ich eine eigene Klasse dafür geschrieben, die nur einmal obige Berechnung ausführt und zwischenspeichert, damit die zukünftigen paint()-Aufrufe schneller ablaufen.

Ich habe nun die Vorschläge ausprobiert (JComponent, Performanceverbesserungen aus DefaultListCellRenderer übernommen, direkt von DefaultListCellRenderer ableiten), jedoch besteht das Problem weiterhin.

Für weitere Denkanstöße wäre ich sehr dankbar!
 
Zuletzt bearbeitet:
Habe nun die/eine Lösung gefunden.

Ich habe eine neue Theorie aufgestellt, weshalb das Flackern auftritt:
Beim Scrollen in die nächste Zeile der Liste durch Selektieren der ersten, nicht sichtbaren Zeile, macht Java zuerst die neue Zeile sichtbar (und zeichnet neu) und setzt erst danach die Selektion neu (und zeichnet abermals neu). Wie gesagt, das sind nur Vermutungen, die ich wegen meinen Beobachtungen aufgestellt habe.
Testweise habe ich einen Workaround ausprobiert, bei dem ich in einem eigenen KeyListener zuerst die Selektion verändert habe und erst danach diese sichtbar gemacht habe. Dadurch ist das Flackern gänzlich verschwunden. Eventuell hilft das ja dem ein oder anderen.

Code:
addKeyListener(new KeyAdapter() {

	public void keyPressed(KeyEvent e) {
		int keyCode = e.getKeyCode();
		if (keyCode == 40) {
			e.consume();
			scrollDown();
		} else if (keyCode == 38) {
			e.consume();
			scrollUp();
		}
	}
});

private void scrollDown() {
	int size = model.getSize();
	if (size == 0) {
		return;
	}

	int maxIndex = size - 1;
	int oldIndex = getSelectedIndex();
	int newIndex;

	if (oldIndex == -1) {
		newIndex = 0;
	} else {
		newIndex = (oldIndex < maxIndex) ? oldIndex + 1 : oldIndex;
	}

	if (newIndex != oldIndex) {
		setSelectedIndex(newIndex);
		ensureIndexIsVisible(newIndex);
	}
}

private void scrollUp() {
	if (model.getSize() == 0) {
		return;
	}

	int oldIndex = getSelectedIndex();
	int newIndex;

	if (oldIndex == -1) {
		newIndex = 0;
	} else {
		newIndex = (oldIndex > 0) ? oldIndex - 1 : 0;
	}

	if (newIndex != oldIndex) {
		setSelectedIndex(newIndex);
		ensureIndexIsVisible(newIndex);
	}
}

Schönen Abend noch!
 
Hallo hallo, ihr lieben CB'ler!

Es gibt Neuigkeiten, die ich demjenigen, der diesen Thread in ein paar Monaten liest, natürlich nicht vorenthalten möchte.

Meine weiter oben vorgestellte Lösung ist ja, wie ich sie ja auch schon tituliert hatte, eher ein Workaround. Nicht gefallen hat mir dabei, dass der KeyEvent konsumiert wurde, sprich nicht an weitere KeyListener der Liste weitergereicht wurde. Das ist unter Umständen überhaupt nicht wünschenswert.
Ich habe mich daher noch einmal tiefer in die Klassen eingegraben und so ziemlich alles abgeleitet und mit Ausgaben versehen, was ich finden konnte. Sicher ist nun zumindest folgendes: Wenn der Benutzer durch das Benutzen der Pfeiltasten Hoch oder Runter am Anfang oder Ende der Liste ein Hoch- bzw Herunterscrollen bewirkt, werden in der Liste folgende Methoden in exakt dieser Reihenfolge aufgerufen:

1.) public void scrollRectToVisible(Rectangle aRect) {}
2.) public void setSelectedIndex(int index) {}

Genau das habe ich ja schon in einem früheren Post als Vermutung aufgestellt. Ich wollte nur noch einmal sicherstellen, dass es diese Vermutung also auch stimmt.

Nun musste also eine Lösung her, die nicht in die Listener der Liste eingreift, aber trotzdem intelligent genug ist, um nur dann eine neue Zeile sichtbar zu machen, wenn diese auch davor schon selektiert wurde. Dafür muss die JList abgeleitet und ein paar Methoden wie folgt überschrieben werden:

Code:
public class CList extends JList {

	// ===================================================================
	// {[> Attributes
	// ==============
	private int visibleRowCount;
	private boolean doEnsureVisibility;



	// ===================================================================
	// {[> Methods (Public)
	// ====================
	public synchronized void scrollRectToVisible(Rectangle aRect) {
		int curIndex = getSelectedIndex();
		int minIndex = locationToIndex(aRect.getLocation());
		int maxIndex = minIndex + visibleRowCount - 1;
		if (curIndex == minIndex || curIndex == maxIndex) {
			super.scrollRectToVisible(aRect);
		} else {
			doEnsureVisibility = true;
		}
	}

	public synchronized void setSelectedIndex(int index) {
		super.setSelectedIndex(index);
		if (doEnsureVisibility) {
			doEnsureVisibility = false;
			Rectangle cellBounds = getCellBounds(index, index);
			if (cellBounds != null) {
				scrollRectToVisible(cellBounds);
			}
		}
	}

	public void setVisibleRowCount(int visibleRowCount) {
		super.setVisibleRowCount(visibleRowCount);
		this.visibleRowCount = visibleRowCount;

	}
}

Das ist bisher die sauberste Lösung, die ich zu dieser Problematik gefunden habe. Hoffentlich hilft die ganze Schreibarbeit dem ein oder anderen weiter ;)

Liebe Grüße!
 

Ähnliche Themen

Zurück
Oben