Cover gallery or List View in Revolucion Library is a JList which dispays cover images. You can see it at the beginning of the Revolucion Library screencast.
First of all, we need to extend JList to achieve a selection change animation. In Revolucion Library I used TimingFramework, but here is the example of how to do the same with a great Trident animation library by Kirill.
ListSelectionListener is used to listen for changes in lists selection value, so we can start the animation when a cover is selected.
selectionBounds represent the current bounds of a selection rectangle, and selectionAlpha is used to control the visibility of the selection rectangle – when there is no items in the list, value is 0.0f.
timeline is the main timeline for animations. animationSpeed is actually a duration of the animation in milliseconds.
public class AnimatedList extends JList implements ListSelectionListener, ComponentListener {
private float selectionAlpha = 0.0f;
private float newSelectionAlpha = 1.0f;
private Rectangle selectionBounds = new Rectangle(0, 0, 0, 0);
private Rectangle currentSelectionBounds = new Rectangle(0, 0, 0, 0);
private Rectangle newSelectionBounds = new Rectangle(0, 0, 0, 0);
private Timeline timeline;
private int animationSpeed = 400;
private int selectionBorderWidth = 1;
private int selectionBorderRoundness = 10;
private Color selectionBorderColor = Color.gray;
public AnimatedList() {
setSelectionBackground(new Color(255, 255, 255, 60));
setSelectionForeground(Color.black);
setOpaque(false);
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
setLayoutOrientation(JList.HORIZONTAL_WRAP);
setVisibleRowCount(-1);
addListSelectionListener(this);
addComponentListener(this);
initAnimation();
}
Next we need to setup the Timeline …
Every time timeline.play() is called, selectionBounds will change from currentSelectionBounds to newSelectionBounds, and selectionAlpha from it’s current value to a newSelectionAlpha value. And we add Repaint callback so JList is repainted when these properties change.
private void initAnimation() {
timeline = new Timeline(this);
timeline.setDuration(animationSpeed);
timeline.addPropertyToInterpolate("selectionBounds", currentSelectionBounds, newSelectionBounds);
timeline.addPropertyToInterpolateTo("selectionAlpha", newSelectionAlpha);
timeline.setEase(new Spline(0.8f));
timeline.addCallback(new Repaint(this));
timeline.addCallback(new TimelineCallbackAdapter() {
@Override
public void onTimelineStateChanged(TimelineState oldState, TimelineState newState, float durationFraction, float timelinePosition) {
if (newState.equals(TimelineState.DONE))
currentSelectionBounds.setBounds(newSelectionBounds);
}
});
}
Then we implement ListSelectionListener and ComponentListener.
@Override
public void valueChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting())
updateSelectionBounds();
}
@Override
public void componentHidden(ComponentEvent e) {}
@Override
public void componentMoved(ComponentEvent e) {
updateSelectionBounds();
}
@Override
public void componentResized(ComponentEvent e) {
updateSelectionBounds();
}
@Override
public void componentShown(ComponentEvent e) {
updateSelectionBounds();
}
And here is what updateSelectionBounds() method does:
public void updateSelectionBounds() {
int index = getSelectedIndex();
// If there is something selected we set the selection bounds to the bounds of the selected cell ...
if (index > -1) {
newSelectionBounds.setBounds(getCellBounds(index, index));
newSelectionAlpha = 1.0f;
}
// ... or if there is no selection, we do this to simulate rectangle dissapearing ...
else {
newSelectionBounds.setBounds(getX() + getWidth() / 2, getY() + getHeight() / 2, 0, 0);
newSelectionAlpha = 0.0f;
}
// ... and than start the animation
if (!timeline.isDone())
timeline.cancel();
timeline.play();
}
And finally, override the paintComponent() to do the selection rectangle painting.
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, selectionAlpha));
g2.setColor(getSelectionBackground());
g2.fillRoundRect(selectionBounds.x, selectionBounds.y, selectionBounds.width, selectionBounds.height, selectionBorderRoundness, selectionBorderRoundness);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(selectionBorderColor);
g2.setStroke(new BasicStroke(selectionBorderWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2.drawRoundRect(selectionBounds.x, selectionBounds.y, selectionBounds.width, selectionBounds.height, selectionBorderRoundness, selectionBorderRoundness);
g2.dispose();
}
Add getters and setters for selectionBounds and selectionAlpha, and that’s it with animations.
To speed up image painting in the JList, I wrote a ListCellRenderer that will cache cover images.
public class CoverListCellRendererTest extends DefaultListCellRenderer {
// http://java.sun.com/javase/6/docs/api/java/lang/ref/SoftReference.html
private HashMap<String, SoftReference<ImageIcon>> thumbnails;
public CoverListCellRendererTest() {
thumbnails = new HashMap<String, SoftReference<ImageIcon>>();
setOpaque(false);
setHorizontalAlignment(JLabel.CENTER);
setHorizontalTextPosition(JLabel.CENTER);
setVerticalTextPosition(JLabel.BOTTOM);
}
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
Movie movie = (Movie) value;
setText(movie.getTitle());
String coverFileName = movie.getCover();
if (coverFileName != null)
setIcon(getThumbnail(coverFileName));
return this;
}
private ImageIcon getThumbnail(String coverFileName) {
ImageIcon icon = null;
SoftReference<ImageIcon> reference = thumbnails.get(coverFileName);
if (reference != null)
icon = reference.get();
if (icon == null) {
try {
BufferedImage image = ImageUtils.getCoverThumb(coverFileName);
icon = new ImageIcon(image);
thumbnails.put(coverFileName, new SoftReference<ImageIcon>(icon));
}
catch (IOException e) {
icon = new ImageIcon(ImageUtils.getEmptyCover());
}
}
return icon;
}
}