Something for the weekend
Dec 2006
Improving the performance of the Carousel
I sent a quick e-mail to Romain guy to let him know how I was re-using his reflection code, and because I thought he might get a kick out of it. I was delighted to get a reply along with some suggestions for improving performance. The main points he raised were as follows:

1. Gradient painting was being done very inefficiently (and he suggested looking at this thread)
2. I should stop using ImageIcon to do the loading, and use ImageIO instead. I've been putting off looking at ImageIO for a long time, seems like a good excuse to have a fiddle
3. The BICUBIC rendering hint gives little improvement over BILINEAR but costs a LOT more
4. Check out SwingX and use their compatible BufferedImage loader for best possible performance

As this is the first time I've fiddled with ImageIO, I'm not going to use SwingX I like to figure things out myself the first time so that I fully understand why the right solution is better. I thought it would be good to perform some performance metrics and see just how much we could speed things up, here are the results. If you missed the original posts on the carousel then just look back in the blog (hum, it was only yesterday!)

The first step is to put some performance metrics in. This required a little bit of thinking as we have a couple of "painters" to see. My first thought was to look at the paint method and time that, it seemed to work well so I dug out an old metric recording class I wrote a long time ago to measure server request performance. It's a useful little class.

/*
* PerformanceMonitor.java
*
* Created on June 12, 2003, 7:36 PM
*
*/

package com.blogofbug.utility;

/**
*
* @author buggles
*/
public class PerformanceMonitor {
private long operationTimes[];
private int nextSlot=-1;
private String monitorName = "Unset";
private long operationStartTime = -1;

/** Creates a new instance of PerformanceMonitor */
public PerformanceMonitor(String monitorName, int samples) {
operationTimes = new long[samples];
nextSlot = 0;
this.monitorName = monitorName;
for (int i = 0;i
operationTimes[i]=-1;
}
}

/**
* Call when an operation started
*/
public void operationStarted(){
operationStartTime = System.currentTimeMillis();
}

/**
* Call when an operation is stopped
*/
public void operationStopped(){
operationTimes[nextSlot++]=System.currentTimeMillis() - operationStartTime;
if (nextSlot == operationTimes.length){
nextSlot = 0;
}
}

/**
* Produces a string containing the key metrics for the operation
*/
public String generateMetrics(){
long totalTime = 0;
long tally = 0;
long fastest = 1000000;
long slowest = 0;
for (int i=0;i
if (operationTimes[i]>-1){
totalTime+=operationTimes[i];
tally++;
fastest = Math.min(fastest, operationTimes[i]);
slowest = Math.max(slowest, operationTimes[i]);
} else{
break;
}
}
int lastSlot = nextSlot - 1;
if (lastSlot == -1){
lastSlot=operationTimes.length-1;
}
return monitorName+" "+tally+" operations - Range: "+fastest+"ms to "+slowest+"ms Total: "+totalTime+"ms Average: "+(totalTime/tally)+"ms Last: "+operationTimes[lastSlot]+"ms";
}
}

It's been trimmed down a little here, I just don't need some of the complexity for this. I plugged it into the paint method of JCarosel

public void paint(Graphics g){
performanceMonitor.operationStarted();
super.paint(g);
performanceMonitor.operationStopped();
System.out.println(performanceMonitor.generateMetrics());
}


and got the following results:

JCarousel.paint() 78 operations - Range: 21ms to 86ms Total: 2442ms Average: 31ms Last: 24ms

So we have a peak performance of 21ms (i.e. slower than 60 fps) and a criminaly slow 86ms performance. Average still very bad. Romain Guy was right, something is badly wrong!

Let's do some easy stuff first, changing the rendering hint from BICUBIC to BILINEAR...

JCarousel.paint() 78 operations - Range: 20ms to 86ms Total: 2348ms Average: 30ms Last: 23ms

Hum, doesn't look like a huge change, but remember that a millisecond we've trimmed off of peak and average performance. Certainly a start and not bad for changing one constant. Let's tackle the gradient next. There's a couple of inefficiencies here, one is I am creating a new gradient paint each time, and the second is how I'm rendering the gradient itself. I'm going to change the code to create and store the gradient painter first.

JCarousel.paint() 78 operations - Range: 20ms to 86ms Total: 2357ms Average: 30ms Last: 23ms

Hum, that looks slightly slower! Given that I'm already not using cyclic gradients (read the blog of Romains... All the way down) it would seem like a good idea to try the other recommended methodology using ImagePaint and stretching the image.

if (cache == null || cache.getHeight() != getHeight()) {
cache = new BufferedImage(2, getHeight(),
BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = cache.createGraphics();

GradientPaint paint = new GradientPaint(0, 0, start,
0, getHeight(), end);
g2d.setPaint(paint);
g2d.fillRect(0, 0, 2, getHeight());
g2d.dispose();
}
g2.drawImage(cache, 0, 0, getWidth(), getHeight(), null);

The results came as a bit of a shock

JCarousel.paint() 78 operations - Range: 26ms to 89ms Total: 3081ms Average: 39ms Last: 35ms

Almost a second slower for just 78 draws, big negative impact. OK, let's try another technique listed on the site using a texture paint....

JCarousel.paint() 78 operations - Range: 20ms to 85ms Total: 2705ms Average: 34ms Last: 28ms

No great improvement, and certainly not faster for this size gradient (and that's probably key). Rather than change the size to suit the results, I'm going to stick with the standard gradient paint, but leave the texture code there, what I would really like to do in the future is some decent instrumentation to see the cross-over point between the two techniques and make it auto-switch. For now, a little disappointed, onto looking at ImageIO and my use of ImageIcon etc.

One of the other points Romain had made was that there was really no need to specify an ImageObserver, so I took that out, not gain, but no pain either

JCarousel.paint() 78 operations - Range: 20ms to 86ms Total: 2357ms Average: 30ms Last: 24ms

So onto ImageIO and creating an appropriately compatible image (with the actual graphics device being used by the OS). Well the ImageIO part turned out to be very very easy, the real trick was digging through the various device and graphics configurations to get that compatible image. To encapsulate this for the various constructors I created a setImage() method that takes a string URL and performs the various steps.

private void setupImage(String imageURL){
Image image = null;
try {
image = ImageIO.read(new URL(imageURL));
} catch (MalformedURLException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
if (image==null){
return;
}

GraphicsConfiguration configuration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();

//Create a buffered image which is the right (translucent) format for the current graphics device, this
//should ensure the fastest possible performance.
BufferedImage originalImage = configuration.createCompatibleImage(image.getWidth(null),image.getHeight(null), Transparency.TRANSLUCENT);

//Blit the loaded image onto the optimized surface by creating a graphics context for the new image
Graphics g = originalImage.getGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();

//First hack, just sets the bufferedIMage to the one loaded, don't cache any rendering
bufferedImage = originalImage;
setPreferredSize(new Dimension(originalImage.getWidth(null), originalImage.getHeight(null)));
}

One quick tweak to remove the dependance on ImageIcon in the paint method, make sure the image created to render the reflection is compatible with the graphics device also and we fire off our metrics again...

JCarousel.paint() 78 operations - Range: 16ms to 55ms Total: 1965ms Average: 25ms Last: 18ms

And we have our first big result, big improvement on both peak and trough performance, and consummate improvement to average render times. There's one more thing we can do, remembering the mantra of graphics programmers everywhere... to make the rendering pipeline as fast as possible, get any and all operations OUT of it. So let's use setImage to correctly create the reflected image and text so that the paint is just a straight scaled blit and the rendering of the text. Resulting in...

JCarousel.paint() 78 operations - Range: 8ms to 35ms Total: 974ms Average: 12ms Last: 11ms

There we have it, I've no doubt that the rendering hints and gradient rendering would have a bigger effect on a larger area, but on a small area you just don't see the benefit. However, creating an image that is already compatible with the final rendering surface brought a real performance bonus, and I just had no idea. I'm pretty sure I can give several of my applications a real boost now using this technique (6ms is significant). However, the biggest hit came by moving things out of the rendering loop altogether.

I guess you still want to know that it looks right? Honestly some people just aren't trusting enough!

Picture 9
|
Nintendo Wii: Gimmic or Gaming Platform?
Our kids unwrapped their Wii on Christmas day, and there's no doubt it was an instant hit with them and the family. I thought I would wait a couple of days to see how usage changed over the following 5 days before posting a review.

The Wii was unwrapped with great excitement, with the kids clamoring to get their hands on it. We had bought them the main console (includes Wii Sports here in Europe) as well as Wii Play (with the extra controller), Zelda, and Monster 4x4 World Circuit (each targeted at the normal game type liked by one of the kids). The initial plug-in was easy, wireless setup to our home router easy, and although I've read some complaints, it was a 5 minute job to download the two system updates, and we were in business.

1

Wii Sports went in first, and I have to say from the first moment, to just now when I finished my Wii Fitness Program for the day it is one of the classic pieces of gaming software developed, and the Wii will stand or fall based on how close to this level of perfection they get. One of the first things we noticed was that we didn't have our own Mii's. Mii's are characters you enter (give them names, define their faces and physical proportions etc) to tie your game saves to. This means Wii Sports (and Wii Play) can track your progress, and of course adds an extra edge when playing two player games; the characters even look like you! So with our newly defined Mii's we huffed and puffed our way to Tennis greatness. Since then we've pushed out further and are big fans of Tennis, Baseball, and Bowling. Golf is great, but it's not a game we take much interest in in the family so I guess it's hard to get as "into" it as we might if we played golf. Boxing... hasn't pushed our buttons at all, again we don't do it or watch it as a family, but I find the controls the least instinctive of all. That said, Boxing does excel as one of the "you'll be exhausted at the end of the game" games. This leads me back to the Wii Fitness and Training. These are mini-master strokes, each day the Wii will set you 3 challenges and measure your sports "Age" with 20 being the optimum (eek, I'm 15 years off my peak!). I've dragged my age down from 64 through the 30's to 27 today! All of the challenges can be practiced in Wii training, and I'm making sure I spend 30 minutes a day doing the various mini-games. I only just get to the stage where you might be considered to be breaking a sweat, but I certainly am breathing harder than usual, and I have muscles that ache. This is a good thing, and leads to my final good thing. The kids stop playing with the console when they are physically tired! Their sessions seem naturally limited to around an hour on Wii Sports before they feel like a bit of a rest and doing something else (they're not bored!). You can bet they'll be fighting over which game they will play first in a couple of hours, but I really like that the console seems to limit how much they play without taking a break.

wii-remote-controller

Wii-Play comes with a spare Wii-Remote (needed for the two player shenanigans) and is intended as a training ground for the Wii remote. That it may be, but few of the games are that engaging. Duck-Shooting deserves a mention, great two player fun, fishing has been popular with the kids, and the pool game has gone down well with the older members of our family. Overall for me, this is a no-score draw of a game, the tedious out-weighs the great (and both are present) and I have been left wanting to play more "levels" of say duck-shoot rather than being subjected to the "Find the matching Mii's" game. Yawn.

notizia.shot1.T150

Monster 4x4 World Circuit does what it says on the tin. It comes with a steering wheel that has to be assembled (not hard) and then has the wiimote plugged into it and away you go, it's like being a kid again pretending to hold a steering wheel and throwing it left and right as you drive along. The game itself is pretty basic, not bringing anything new to the "not-very-real-racing" genre. Stunts can be performed, Mario Kart-esque upgrades etc, but as I hoped, the control system lends itself to a 4 year old being able to play it with ease. It may not be setting the older kids world alight, but it's great for the very young.

Finally, onto the big launch title, Zelda: Twighlight Princess. I've never been into Zelda, and there are very few of these types of game that I enjoy. Zelda doesn't really do anything to change that for me. The graphics look like a very good PS2 title (although smoother) with lots of High Dynamic Range lighting and they are fast an fluid with attractive spot effects. The music is what you would expect, but all eyes turn to the control system, and the game play.

zelda_twilight_princess_wii_demo_e3

Sure enough the Wii's unique control system is put to good use. The combat system works very well, more instinctively than I expected. On screen your character's sword is strapped to his back... so how do you equip it ready for a fight? Well you reach over your shoulder with the Wii-mote of course. Very nice, and leads to one of many Wii moments when the kids were trying to work out which button to press, before one of them says "just do what you'd do" and hey presto it works (another was serving in Wii Sports tennis. Just literally toss up and smash... almost too obvious!). The game... oh the game. Kids are enjoying it, although it bores me to tears, gushing emotion, cut scenes you can't quickly skip through. Our eldest daughter laps it up though, and is clearly having a lot of fun. For me, the control system remains the highlight (fishing very good, oh yes).
wii_internet


That's some of the key launch titles covered, what about the other Wii features? The beta internet channel works very very well (better than it has any right to) and I find myself looking forward to being able to surf on the Wii with Google Reader to keep up on my blogs. Very nicely done Nintendo/Opera. The weather channel is a very nice gimmick, filling up space on what, at launch, is a very empty menu system. The photo-channel is cute, slot in a SD card with some photos on, draw all over them and post them on the Wii message board for the other members of your family to see. Cute. which leads neatly onto the Wii Message board. Your Wii sends you messages, letting you know how long which games have been played, new personal bests in the Wii Sports/Wii Play games, and it can be synced with an actual email address. You can even send e-mail to others from it which is a nice touch. Which brings us to how keyboard entry works, no great surprise there is an onscreen keyboard you point and click at. It works very well, and even sports predictive typing which can really speed things up. One can't help but feel Nintendo are deploying lessons learned from the DS which has an almost identical system.

images

Finally, the hardware. Nintendo have taken a risk, they've not packed the console full of more graphics power than an SGI christmas party. What they have packed it full of is standard interfaces. The Wiimotes are Bluetooth (wireless keyboards and mice to come then), the memory cards are standard SD cards (meaning we didn't bother buying one for the Wii, just used an old 1Gb one lying around from a smart phone), and of course there are USB ports and the 802.11 wireless network. What this means is that Nintendo have made it very easy for companies to build add-ons. As we've seen with the iPod this can be critically important to have a healthy eco-system of supporting products. You can also use all of your GameCube controllers with a "flip-flap" opening to reveal for controller ports and two memory slots. That pleased the kids too. But what of the graphics and sound? Absence of a hard-drive? To be honest, I don't care. I'm sure we will buy a 360 or PS3 at some point when the price has come down, but right now the Wii offers the first novel gaming experience since the PS1 launched. To be blunt, screw the graphics they are good enough. Sounds is what sound is, although hard-core gamers may be searching for 5.1 surround sound, our kids don't care and they are rather keen on the full 3D control system.

So what's the summary? The Wii itself is genius, and provides a unique platform for future gaming and media fun. It's going to initially capture imaginations and sell like hot cakes. However, the next 12 months are critical. Nintendo need to build a solid base of 4 or 5 killer Wii only games that make use of the unique control system and ensure that the number of consoles out there keep growing. I can see them leaning on their DS experience to do that (and some synergy there would be great), but it's not an easy shot. The PS3 will gain market share, and the XBox 360 is another master piece for the older gamer. For me, the weakest machine at this point is the PS3, but if Nintendo play it right, they'll ensure the 360 and PS3 fight it out between them while it races ahead. Get it wrong, and in 18 months the Wii will be an interesting milestone is game console history, and little more.

Right, I'm off to see if I can get to Pro level at Wii Tennis.
|
Java Carousel Part 3: Reflected Images Love Carousels
Although our carosel can take any Java component, what we really need is a dedicated component that looks perfect in the carosel? For example, the image shouldn't be clipped as in our previous examples, it should scale to fit the size the layout manager sets for it. We also need labels and of course that trade mark "wet floor" reflection (original algorithm came from an article by Romain Guy).

As some of you may have picked up by now, I like to build complexity up gradually. So I'm going to start with a new Component called ImageLabel. What's special? It's basically a standard JLabel, but its constructor takes an image, and the component's preferred size is set to the dimensions of that image. Alternatively, if that's not desired (i.e. you want to specify a higher resolution image and ensure it's always scaled down) you can supply a target preferred size. OK, none of that is very different, so the real difference comes in the paint method. It scales the image to fill the set size, meaning as our CaroselLayout sizes the components as they spin around, it will fill the space available to it.

public class ImageLabel extends JLabel{
protected ImageIcon imageIcon = null;

/**
* Creates a new instance of ImageLabel. The prefered width and height will
* be set to the dimensions of the image
*
* @param icon The image to display
*/
public ImageLabel(ImageIcon icon) {
super(icon);
this.imageIcon = icon;
setPreferredSize(new Dimension(icon.getIconWidth(), icon.getIconHeight()));
}

/**
* Creates a new instance of ImageLabel, setting the preferred rendering size to
* the supplied dimensions
*
* @param icon The image to place on the label
* @param width The prefered width
* @parem height The prefered height
*/
public ImageLabel(ImageIcon icon, int width, int height){
this(icon);
setPreferredSize(new Dimension(width,height));
}

/**
* Paints the label scaling the image to the appropriate size
*
*/
public void paint(Graphics graphics) {
Image image = this.imageIcon.getImage();
ImageObserver observer = imageIcon.getImageObserver();
((Graphics2D)graphics).setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
((Graphics2D)graphics).setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
graphics.drawImage(image, 0,0,getWidth(),getHeight(),0,0,image.getWidth(observer),image.getHeight(observer),observer);
}
}



Pasted Graphic 4

Here we are with a Carosel with 4 of the normal ImageLabels added. It's looking a lot more like our front Row component already, but we need to add reflections and labels. Let's do that by extending our ImageLabel class with ReflectedImageLabel.

public class ReflectedImageLabel extends ImageLabel{
//The text title to display
private String text = "";
private static final Font reference = new Font("Arial",Font.BOLD,14);


/**
* See constructor for image label
*
*/
public ReflectedImageLabel(ImageIcon image){
super(image);
}

public ReflectedImageLabel(ImageIcon image, int width, int height){
super(image,width,height);
}

public ReflectedImageLabel(ImageIcon image, String text, int width, int height){
this(image,width,height);
this.text=text;
}

public Dimension getPreferredSize() {
Dimension d = super.getPreferredSize();
d.height = (int) ((double) d.height * 1.5);

return d;
}

public void paint(Graphics graphics) {
Graphics2D g = (Graphics2D) graphics;
Image image = this.imageIcon.getImage();
ImageObserver observer = imageIcon.getImageObserver();

int drawHeight = (int) ((double) getHeight() / 1.5);

g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);

g.drawImage(image, 0,0,getWidth(),drawHeight,0,0,image.getWidth(observer),image.getHeight(observer),observer);



BufferedImage reflection;
try{
reflection= new BufferedImage(getWidth(), drawHeight, BufferedImage.TYPE_INT_ARGB);
} catch (Exception e){
return;
}
Graphics2D reflectionGraphics = reflection.createGraphics();

AffineTransform tranform = AffineTransform.getScaleInstance(1.0, -1.0);
tranform.translate(0, -drawHeight);

// draw the flipped image
AffineTransform oldTransform = reflectionGraphics.getTransform();
reflectionGraphics.setTransform(tranform);
//Should be drawn upside down
//could also try
//super.paint(reflectionGraphics)
reflectionGraphics.drawImage(image, 0,0,getWidth(),drawHeight,0,0,image.getWidth(observer),image.getHeight(observer),observer);


reflectionGraphics.setTransform(oldTransform);


GradientPaint painter = new GradientPaint(0.0f, 0.0f,
new Color(0.0f, 0.0f, 0.0f, 0.5f),
0.0f, drawHeight / 2.0f,
new Color(0.0f, 0.0f, 0.0f, 1.0f));

// this use : Ar = Ad*(1-As) and Cr = Cd*(1-As)
reflectionGraphics.setComposite(AlphaComposite.DstOut);
reflectionGraphics.setPaint(painter);
// this will make our image transluent ...
reflectionGraphics.fill(new Rectangle2D.Double(0, 0, getWidth(), drawHeight));

//now draw on the main graphics layer
graphics.drawImage(reflection, 0,drawHeight,observer);

//Debugging
/*
String text = this.getText();
if (text==null)
text="Null";
char chars[] = new char[text.length()];
text.getChars(0,text.length(),chars,0);
graphics.drawChars(chars,0,chars.length,10,getHeight()/2);*/

reflectionGraphics.dispose();

//Draw text if there is any...
if (text.length()>0){
Graphics2D g2d = (Graphics2D) graphics;
Rectangle2D bounds = reference.getStringBounds(text,g2d.getFontRenderContext());
double scaleFactor = (double) getWidth()/ bounds.getWidth();
Font font = new Font("Arial",Font.BOLD,(int) (14.0 * scaleFactor));
g2d.setFont(font);
graphics.setColor(Color.WHITE);
graphics.drawString(text,0,drawHeight+(int)(bounds.getHeight()*scaleFactor));
}
}
}


Once again, nothing very complex here. The constructors basically match those for ImageLabel, with additional provision for text to show in the label (not reflected), and then re-use a piece of code I must have abused a hundred times by now, Romain Guy's reflection code.

public class ReflectedImageLabel extends ImageLabel{
//The text title to display
private String text = "";

private static final Font reference = new Font("Arial",Font.BOLD,14);


/**
* See constructor for image label
*
*/
public ReflectedImageLabel(ImageIcon image){
super(image);
}

public ReflectedImageLabel(ImageIcon image, int width, int height){
super(image,width,height);
}

public ReflectedImageLabel(ImageIcon image, String text, int width, int height){
this(image,width,height);
this.text=text;
}

public Dimension getPreferredSize() {
Dimension d = super.getPreferredSize();
d.height = (int) ((double) d.height * 1.5);

return d;
}

public void paint(Graphics graphics) {
Graphics2D g = (Graphics2D) graphics;
Image image = this.imageIcon.getImage();
ImageObserver observer = imageIcon.getImageObserver();

int drawHeight = (int) ((double) getHeight() / 1.5);

g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);

g.drawImage(image, 0,0,getWidth(),drawHeight,0,0,image.getWidth(observer),image.getHeight(observer),observer);

BufferedImage reflection;
try{
reflection= new BufferedImage(getWidth(), drawHeight, BufferedImage.TYPE_INT_ARGB);
} catch (Exception e){
return;
}
Graphics2D reflectionGraphics = reflection.createGraphics();

AffineTransform tranform = AffineTransform.getScaleInstance(1.0, -1.0);
tranform.translate(0, -drawHeight);

// draw the flipped image
AffineTransform oldTransform = reflectionGraphics.getTransform();
reflectionGraphics.setTransform(tranform);
//Should be drawn upside down
//could also try
//super.paint(reflectionGraphics)
reflectionGraphics.drawImage(image, 0,0,getWidth(),drawHeight,0,0,image.getWidth(observer),image.getHeight(observer),observer);


reflectionGraphics.setTransform(oldTransform);


GradientPaint painter = new GradientPaint(0.0f, 0.0f,
new Color(0.0f, 0.0f, 0.0f, 0.5f),
0.0f, drawHeight / 2.0f,
new Color(0.0f, 0.0f, 0.0f, 1.0f));

// this use : Ar = Ad*(1-As) and Cr = Cd*(1-As)
reflectionGraphics.setComposite(AlphaComposite.DstOut);
reflectionGraphics.setPaint(painter);
// this will make our image transluent ...
reflectionGraphics.fill(new Rectangle2D.Double(0, 0, getWidth(), drawHeight));

//now draw on the main graphics layer
graphics.drawImage(reflection, 0,drawHeight,observer);
reflectionGraphics.dispose();

//Draw text if there is any...
if (text.length()>0){
Graphics2D g2d = (Graphics2D) graphics;
Rectangle2D bounds = reference.getStringBounds(text,g2d.getFontRenderContext());
double scaleFactor = (double) getWidth()/ bounds.getWidth();
Font font = new Font("Arial",Font.BOLD,(int) (14.0 * scaleFactor));
g2d.setFont(font);
graphics.setColor(Color.WHITE);
graphics.drawString(text,0,drawHeight+(int)(bounds.getHeight()*scaleFactor));
}
}
}

And this brings us all the way back to our first screen shot and example...

Picture 2

Looks good doesn't it? There's lot's of optimization that can be done (caching the reflected image rather than redrawing it each time for example) but I shall leave that to the reader. If you use this code please drop in a link to the blog (much appreciated), and enjoy.

Download the demo or The Whole Project

Note: The iTunes CD Images used in this example are reproduced here by kind permission of Brian Zeitler from PixelNet Design and please note that all copyright remains with Brian and PixelNet Design.
|
Java Carousel Part 2: Creating a Carousel Component
We can now layout and animate our components using a layout manager, but we need something that can cause the animations to occur, and perhaps looks a little better. Queue our Gradient Panel and introducing JCarosel.

We want our component to be derived from a JPanel (it contains all of the container functionality we need, no pun intended), but we also know we want it to look pretty. So the first thing I'm going to do use a GrandientPanel class I've written for this purpose several times, here it is for completeness.

public class GradientPanel extends JPanel{
protected Color start;
protected Color end;


public void setBackground(Color color){
this.start = color;
this.end = color;
super.setBackground(color);
}

public void setBackground(Color start, Color end){
this.start = start;
this.end = end;
}

public void paint(Graphics graphics) {
if (start == end){
super.paint(graphics);
return;
}

Graphics2D g = (Graphics2D) graphics;

Rectangle2D.Double rectangle = new Rectangle2D.Double();
rectangle.setRect(0, 0, getWidth(), getHeight());
GradientPaint gradient = new GradientPaint((float)(getWidth()/2), (float) getY(), start, (float) (getWidth()/2), (float) getHeight(), end, false);

g.setPaint(gradient);
g.fill(rectangle);
super.paintChildren(graphics);
}

}


It's not very complicated, just using a custom paint method to draw a gradient instead of a single color background. But it's already starting to look a little better!

Picture 5

So our carosel component will extend this gradient panel class, let's take a look at it in full...

public class JCarosel extends GradientPanel implements MouseListener{
public static final String FRONT_COMPONENT_CHANGE = "frontComponentChanged";
protected CaroselLayout layout;

/**
* Creates a new instance of JCarosel
*/
public JCarosel() {
layout = new CaroselLayout(this);
this.setLayout(layout);
}

public Component add(Component component){
add("",component);
bringToFront(getComponent(0));
validate();
return component;
}

public void remove(Component component) {
super.remove(component);
if (getComponentCount()>0){
bringToFront(getComponent(0));
}
invalidate();
validate();
}

public Component add(ImageIcon icon, int width, int height){
ReflectedImageLabel component = new ReflectedImageLabel(icon,width,height);
component.addMouseListener(this);
return add(component);
}

public Component add(ImageIcon icon, String text, int width, int height){
ReflectedImageLabel component = new ReflectedImageLabel(icon,text,width,height);
component.addMouseListener(this);
return add(component);
}

public void bringToFront(Component component){
firePropertyChange(FRONT_COMPONENT_CHANGE,getComponent(0),component);
layout.setFrontMostComponent(component);
}

/**
* Which component is at the front
*/
public Component getFrontmost(){
return getComponent(0);
}

/**
* Bring the "clicked" component to the front
*/
public void mouseClicked(MouseEvent mouseEvent) {
if (mouseEvent.getClickCount()==1){
bringToFront((Component) mouseEvent.getSource());
}
}

/**
* Moves everything to their final positions
*
*/
public void finalizeLayoutImmediately(){
layout.layoutContainer(this);
layout.finalizeLayoutImmediately();
repaint();
}

/** Not interested */
public void mousePressed(MouseEvent mouseEvent) {}
/** Not interested */
public void mouseReleased(MouseEvent mouseEvent) {}
/** Not interested */
public void mouseEntered(MouseEvent mouseEvent) {}
/** Not interested */
public void mouseExited(MouseEvent mouseEvent){}
}


Not too much to try to understand is it? Well, hopefully not. Once again, let's break it down. As already discussed it extends GradientPanel, but also implements MouseLIstener. Why? Well what we want to make our carosel spin when the user clicks on an component on the carosel to bring that component to the front (remember setFrontmostComponent?) and we'll use a mouse listener to do that. First things first, let's look at the constructor...

/**
* Creates a new instance of JCarosel
*/
public JCarosel() {
layout = new CaroselLayout(this);
this.setLayout(layout);
}


Simple really, we construct and use a CaroselLayout object. We need to keep a reference to the layout manager so we can call that setFrontmostComponent method later.

public Component add(Component component){
component.addMouseListener(this);
add("",component);
bringToFront(getComponent(0));
validate();
return component;
}

public void remove(Component component) {
super.remove(component);
component.removeMouseListener(this);
if (getComponentCount()>0){
bringToFront(getComponent(0));
}
invalidate();
validate();
}


I also override the add and remove methods inherited from Container. We want to animate if that happens, bringing the newly added component to the front.

The next two methods we'll look at in more detail in the next installment, but for now you just need to know that we are going to have some methods for easily adding nice image buttons to look similar to the FrontRow buttons without a lot of over-head.

public Component add(ImageIcon icon, int width, int height){
ReflectedImageLabel component = new ReflectedImageLabel(icon,width,height);
component.addMouseListener(this);
return add(component);
}

public Component add(ImageIcon icon, String text, int width, int height){
ReflectedImageLabel component = new ReflectedImageLabel(icon,text,width,height);
component.addMouseListener(this);
return add(component);
}


So finally, and oh-so-Java wonderfully simply... the code to spin around our carosel when they click on a component...

/**
* Bring the "clicked" component to the front
*/
public void mouseClicked(MouseEvent mouseEvent) {
if (mouseEvent.getClickCount()==1){
bringToFront((Component) mouseEvent.getSource());
}
}

That's it. In the final installment I'll introduce the ReflectedImage component (and you'll know why give the citation to Romain Guy too!) and make the jar and source code available.

Pasted Graphic 3

Note: The iTunes CD Images used in this example are reproduced here by kind permission of Brian Zeitler from PixelNet Design and please note that all copyright remains with Brian and PixelNet Design.
|
Java Carousel Part 1: Layout and Animation
In the first installment we looked at what we needed to do to recreate the FrontRow-esque carosel as a standard Swing component. The first step is the LayoutManager, followed by the animation to rotate the carosel.

So without any more waffle, let's start with our Layout Manager

The first thing we need is a class to test our layout manager and a couple of components to it, here's that code (you'll need it to test!)

public class CaroselLayoutManager extends JFrame{

/** Creates a new instance of CaroselLayoutManager */
public CaroselLayoutManager() {
super("Carosel Layout Manager");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(600,400);
getContentPane().setLayout(new CaroselLayout(getContentPane()));
getContentPane().add("Label Example",new JLabel("Example with text"));
getContentPane().add("Button Example", new JButton("Oh, and a button too!"));
getContentPane().add("Text Field", new JTextField("Edit me!"));
getContentPane().add("Image example",new JLabel(new ImageIcon(CaroselLayoutManager.class.getResource("/com/blogofbug/examples/carosel/itunes.png"))));
}

/**
* @param args the command line arguments
*/
public static void main(String args[]) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new CaroselLayoutManager().setVisible(true);
}
});
}
}


Inheriting from a JFrame, the constructor sets the layout manager to our CaroselLayout (coming soon!) and then adds a couple of standard components to it, that should be enough to test (thanks again to PixelNet Design for permission to use the images). Nothing too exciting here, so what does the layout manager class look like?

public class CaroselLayout implements LayoutManager,ActionListener{
protected int numberOfItems = 0;

protected LinkedList components = new LinkedList();

protected Hashtable additionalData = new Hashtable();

protected double rotationalOffset = 0.0;

protected double targetOffset = 0.0;

private Timer animationTimer = null;

private Container container = null;

public CaroselLayout(Container forContainer){
animationTimer = new Timer(0,this);
container = forContainer;
}

/**
* Name is ignored
*
*/
public void addLayoutComponent(String name, Component comp) {
components.addLast(comp);
recalculateCarosel();
}

/**
* Remove the component
*
*/
public void removeLayoutComponent(Component comp) {
components.remove(comp);
recalculateCarosel();
}

/**
* Gets the additional data stored by the layout manager for a given component
*
* @param comp The component you wish retreive the data for
* @return A position, which is added if it does not already exist. Never null unless
* you run out of memory!
*/
protected CaroselPosition getPosition(Component comp){
CaroselPosition cpos = (CaroselPosition) additionalData.get(comp);

if (cpos==null){
cpos = new CaroselPosition(comp);
additionalData.put(comp,cpos);
}

return cpos;
}

protected int recalculateVisibleItems(){
int visibleItems=0;
try{
for (Component comp : components){
if (comp.isVisible()){
visibleItems++;
}
}
} catch (ConcurrentModificationException ex){
return recalculateVisibleItems();
}
return visibleItems;
}

protected void recalculateCarosel(){
//Need to count visible, not just how many in the list
//Again dealing with out-of-EDT modification
numberOfItems = recalculateVisibleItems();

//Trap and re-calc on concurrent modification (might as well be up-to-date)

try{
boolean animate=false;
double itemCount = 0;
for (Component comp : components){
CaroselPosition position = getPosition(comp);
if (comp.isVisible()){
double localAngle = itemCount * (Math.PI * 2.0 / (double) numberOfItems);
position.setAngle(localAngle);
}
if (position.isAnimating()){
animate=true;
}
itemCount+=1.0;
}

//If we do need to animate, get it started
if (animate){
animationTimer.start();
}
} catch (ConcurrentModificationException ex){
recalculateCarosel();
return;
}

}

/**
* Cheats and bases it's size on the prefered sizes of each component
*
*/
public Dimension minimumLayoutSize(Container parent) {
return preferredLayoutSize(parent);
}

/**
* Determine the widest and tallest dimensions, then return the height as 1.5 * the highest, and 3 * the widest
*
* @param parent The container for the layout
*/
public Dimension preferredLayoutSize(Container parent) {
Dimension dim = new Dimension(0, 0);
// get widest preferred width for left && right
// get highest preferred height for left && right
// add preferred width of middle
int widestWidth = 0;
int highestHeight = 0;

Iterator i = components.iterator();
while (i.hasNext()){
Component comp = (Component) i.next();

if (comp.isVisible()){
widestWidth = Math.max(widestWidth, comp.getPreferredSize().width);
highestHeight = Math.max(highestHeight, comp.getPreferredSize().height);
}
}

dim.width = widestWidth * 3;
dim.height = highestHeight * 2;

Insets insets = parent.getInsets();
dim.width += insets.left + insets.right;
dim.height += insets.top + insets.bottom;

return dim;
}


/**
* Lays out all of the components on the carosel. Using the preferred width and height to base
* scaling on
*/
public void layoutContainer(Container target) {
//Local copy of components to avoid concurrent modification
//which could happen if someone adds something to the layout outside
//of the EDT. This is faster than do any synchronization or brute force
//exception catching
LinkedList components = (LinkedList) this.components.clone();
int numberOfItems = this.numberOfItems;

recalculateCarosel();
// these variables hold the position where we can draw components
// taking into account insets
Insets insets = target.getInsets();
int width = target.getSize().width - (insets.left + insets.right);
int height = target.getSize().height - (insets.top + insets.bottom);

int widestWidth = 0;
int highestHeight = 0;


Iterator i = components.iterator();
while (i.hasNext()){
Component comp = (Component) i.next();

if (comp.isVisible()){
widestWidth = Math.max(widestWidth, comp.getPreferredSize().width);
}
}
width -= widestWidth;

int radiusX = width /2;
int radiusY = radiusX / 3;

int centerX = (insets.left+widestWidth/2) + width/2;
int centerY = insets.top + height/2;

//Go through each visible component and set the scale and z-order, and eventually the bounds
//Need to protected against other things adding components at the same time
i = components.iterator();
int p = 0;
CaroselPosition z_order[] = new CaroselPosition[numberOfItems];
while (i.hasNext()){
Component comp = (Component) i.next();
CaroselPosition position = getPosition(comp);
double finalAngle = position.getAngle()+this.rotationalOffset;

double x = (Math.sin(finalAngle) * (double) radiusX)+(double) centerX;
double y = (Math.cos(finalAngle) * (double) radiusY)+(double) centerY;

double s = (y / (double) centerY);
double boundsWidth = (double) comp.getPreferredSize().width * s;
double boundsHeight = (double) comp.getPreferredSize().height * s;

comp.setBounds((int)x - ((int)boundsWidth/2),(int) y - ((int)boundsHeight /2),(int) boundsWidth, (int) boundsHeight);

position.setZ(s);
z_order[p++] = position;
}

//Now sort out the z, we may need to cache the dimensions, do the z and then reset the bounds, see what happens on redraw first
//bubble sort is actually very fast for a small number of items, and this layout shouldn't be used for loads.
boolean swaps = true;
int limit = numberOfItems-1;
while (swaps){
swaps = false;
for (int j=0;j
if (z_order[j].getZ()
CaroselPosition temp = z_order[j+1];
z_order[j+1]=z_order[j];
z_order[j]=temp;
swaps=true;
}
}
limit--;
//We must be done if we hit the bottom
if (limit==0){
swaps=false;
}
}

//Re-order everything (yet as little as possible Happy
for (int j=0;j
if (target.getComponentZOrder(z_order[j].getComponent())!=j){
target.setComponentZOrder(z_order[j].getComponent(),j);
}
}
}

/**
* Returns the current rotational angle
*
* @return The current rotated angle in radians
*/
public double getAngle() {
return this.rotationalOffset;
}

/**
* Sets the current rotational angle. Will not cause an animation to start
*
* @param d The desired angle in radians
*/
public void setAngle(double d) {
this.rotationalOffset = d;
}

/**
* Determines if an animation is currently playing
*
* @return true if it is animating, false if it isn't
*/
protected boolean isAnimating(){
if (!animationTimer.isRunning()){
return false;
}

try{
for (Component comp : components) {
CaroselPosition cpos = getPosition(comp);
if (cpos.isAnimating()){
return true;
}
}
} catch (ConcurrentModificationException ex){
return isAnimating();
}

if (Math.abs(rotationalOffset - targetOffset) < 0.001){
return false;
} else {
return true;
}

}

/**
* Manages timer actions, terminating the timer if any event is fully achieved
*
* @param actionEvent the action event, although this will always be the timer
*/
public void actionPerformed(ActionEvent actionEvent) {
if (animationTimer==null){
return;
}

if (!animationTimer.isRunning()){
return;
}

if (!isAnimating()){
animationTimer.stop();
return;
}

//Update any animating icons, could be subject to modification
//outside the EDT
try {
for (Component comp : components) {
CaroselPosition cpos = getPosition(comp);

if (cpos.isAnimating()){
cpos.updateAngle();
}
}
} catch (ConcurrentModificationException cMe){
actionPerformed(actionEvent);
}
rotationalOffset += (targetOffset - rotationalOffset) / 6.0;
if (container!=null){
this.layoutContainer(container);
if (container instanceof Component){
((Component) container).repaint();
}
}
}

/**
* Moves everything to their "target" positions, without animating anything
*
*/
public void finalizeLayoutImmediately(){
for (Component comp : components){
CaroselPosition cpos = getPosition(comp);

cpos.angle=cpos.targetAngle;
}
rotationalOffset = targetOffset;
recalculateCarosel();
container.validate();
}

/**
* Sets a target angle to rotate to, always choses a direction that is less than
* or equal to 180 degrees
*
* @parm target The target angle in radians
*/
private void setTarget(double target){
//We should never have to rotate more than PI radians
while (Math.abs(target-rotationalOffset) > Math.PI){
if (target
target += Math.PI * 2;
} else {
target -= Math.PI * 2;
}
}
targetOffset = target;
if (!animationTimer.isRunning()){
animationTimer.setCoalesce(true);
animationTimer.setRepeats(true);
animationTimer.setDelay(20);
animationTimer.start();
}
}

/**
* Moves the specified component to the front
*
* @param component The component move to the front
*/
public void setFrontMostComponent(Component component){
setTarget(-getPosition(component).getTargetAngle());
}

protected class CaroselPosition{
protected double angle;
protected double scale;
protected double z;
protected Component component;
protected boolean firstSet = false;
protected double targetAngle = 0.0;

public CaroselPosition(Component component){
angle = 0.0;
scale = 0.0;
z = 0.0;
this.component = component;
}

public Component getComponent(){
return component;
}

public double getZ(){
return z;
}

public void setZ(double z){
this.z = z;
}

public double getTargetAngle(){
return targetAngle;
}

public double getAngle(){
return angle;
}

public double getScale(){
return scale;
}

public boolean isAnimating(){
if ((Math.abs(angle - targetAngle) < 0.001)){
return false;
}
return true;
}

public void moveToTarget(){
angle=targetAngle;
}

public void updateAngle(){
if ((Math.abs(angle - targetAngle) < 0.001)){
angle = targetAngle;
} else {
angle += Math.min((targetAngle - angle) / 6.0,0.10);
}
}

public void setAngle(double angle){
if (firstSet){
this.angle = angle;
this.targetAngle = angle;
firstSet = false;
} else {
this.targetAngle = angle;
}
}

public void setScale(double scale){
this.scale = scale;
}
}

}


Still with me? OK, let's break it down and go through it a bit at a time.

public CaroselLayout(Container forContainer){
animationTimer = new Timer(0,this);
container = forContainer;
}


The constructor takes a parameter which is the container it is responsible for laying out. This is needed because our layout manager is going to be responsible for looking after it's own animated transitions, and needs to tell the container to redraw itself when it does animate. So we keep a record of that. Next we will need a swing Timer to actually update the animations (more of these later), right now we just create it.

Our layout manager implements the LayoutManager interface (OK I'm lazy, there are less methods to implement than LayoutManager2), so there are some methods we have to implement....

/**
* Name is ignored
*
*/
public void addLayoutComponent(String name, Component comp) {
components.addLast(comp);
recalculateCarosel();
}

/**
* Remove the component
*
*/
public void removeLayoutComponent(Component comp) {
components.remove(comp);
recalculateCarosel();
}

These first two add and remove components from the layout. We are keeping our own record of the components, so we add or remove from our list, and then call recalculateCarosel() which determines some of the basic parameters of our layout. Let's take a look at that now:

protected void recalculateCarosel(){
//Need to count visible, not just how many in the list
//Again dealing with out-of-EDT modification
numberOfItems = recalculateVisibleItems();

//Trap and re-calc on concurrent modification (might as well be up-to-date)

try{
boolean animate=false;
double itemCount = 0;
for (Component comp : components){
CaroselPosition position = getPosition(comp);
if (comp.isVisible()){
double localAngle = itemCount * (Math.PI * 2.0 / (double) numberOfItems);
position.setAngle(localAngle);
}
if (position.isAnimating()){
animate=true;
}
itemCount+=1.0;
}

//If we do need to animate, get it started
if (animate){
animationTimer.start();
}
} catch (ConcurrentModificationException ex){
recalculateCarosel();
return;
}

}

The first thing it does is count the number of visible items (just iterates through our component list, counting each one that is visible). We then go through the components list, spacing out each component around a circle. The quicker ones amongst you will have noticed the getPosition method. Basically we want to store extra information about each the components in our layout (their position around the carosel circle, for animations where they need to be, and where they are now, their current size, and their z-order (how close to the front they are)). The getPosition() method retrieves this information for our component from that list.

It's also worth noting that when we set the angle it should be at, the CaroselPosition class has an idea of a target angle (where it needs to be) and a current angle (where it is now).

Is anyone of the components is going to need animating, then the animation timer is started. We'll see what that does later.

There are some other methods we need to implement for a layout manager, specifically determining the maximum, minimum and prefered layout size. I'm cheating, and only determining the prefered size, and saying everything else is equal to that.

/**
* Cheats and bases it's size on the prefered sizes of each component
*
*/
public Dimension minimumLayoutSize(Container parent) {
return preferredLayoutSize(parent);
}

/**
* Determine the widest and tallest dimensions, then return the height as 1.5 * the highest, and 3 * the widest
*
* @param parent The container for the layout
*/
public Dimension preferredLayoutSize(Container parent) {
Dimension dim = new Dimension(0, 0);
// get widest preferred width for left && right
// get highest preferred height for left && right
// add preferred width of middle
int widestWidth = 0;
int highestHeight = 0;

Iterator i = components.iterator();
while (i.hasNext()){
Component comp = (Component) i.next();

if (comp.isVisible()){
widestWidth = Math.max(widestWidth, comp.getPreferredSize().width);
highestHeight = Math.max(highestHeight, comp.getPreferredSize().height);
}
}

dim.width = widestWidth * 3;
dim.height = highestHeight * 2;

Insets insets = parent.getInsets();
dim.width += insets.left + insets.right;
dim.height += insets.top + insets.bottom;

return dim;
}

The more interesting method is the preferredLayoutSize(), basically we look at the preferred size of each of the components to be displayed, and find the tallest one, and the widest one (well their size anyway). We don't want a perfect circle for our carosel, and we want room for at least two of our tallest/widest and highest to be next to each other... that's our base, but as already stated we also want our circle to be an elipse, so we multiple our width by 3 instead of 2.

Finally we add the insets of the container on and return that.

Just one more required method, and it's a big one! layoutContainer needs to set the position and size of every component in the layout, and as such it's the real meat of our method.

Let's break it up and look at it in stages, first let's look at the basic setup that is performed

/**
* Lays out all of the components on the carosel. Using the preferred width and height to base
* scaling on
*/
public void layoutContainer(Container target) {
//Local copy of components to avoid concurrent modification
//which could happen if someone adds something to the layout outside
//of the EDT. This is faster than do any synchronization or brute force
//exception catching
LinkedList components = (LinkedList) this.components.clone();
int numberOfItems = this.numberOfItems;

recalculateCarosel();
// these variables hold the position where we can draw components
// taking into account insets
Insets insets = target.getInsets();
int width = target.getSize().width - (insets.left + insets.right);
int height = target.getSize().height - (insets.top + insets.bottom);


We make a local copy of the components list so that if something gets added or removed in another thread while we are laying out we don't get a concurrent modification exception. Then we determine how big the container is (minus the indents). So, now we know what we have to work with, we need to determine a few parameters about what we want to add.

int widestWidth = 0;

Iterator i = components.iterator();
while (i.hasNext()){
Component comp = (Component) i.next();

if (comp.isVisible()){
widestWidth = Math.max(widestWidth, comp.getPreferredSize().width);
}
}
width -= widestWidth;

We know that the major axis is the horizontal one, so we only consider the width. After determining the widest component, we take that off the width.

int radiusX = width /2;
int radiusY = radiusX / 3;

What is the radius of our elipse (on both axis). Again, the Y is scaled more than the X axis.

int centerX = (insets.left+widestWidth/2) + width/2;
int centerY = insets.top + height/2;

The center of our container around which all of components are laid out.

//Go through each visible component and set the scale and z-order, and eventually the bounds
//Need to protected against other things adding components at the same time
i = components.iterator();
int p = 0;
CaroselPosition z_order[] = new CaroselPosition[numberOfItems];
while (i.hasNext()){
Component comp = (Component) i.next();
CaroselPosition position = getPosition(comp);
double finalAngle = position.getAngle()+this.rotationalOffset;

double x = (Math.sin(finalAngle) * (double) radiusX)+(double) centerX;
double y = (Math.cos(finalAngle) * (double) radiusY)+(double) centerY;

double s = (y / (double) centerY);
double boundsWidth = (double) comp.getPreferredSize().width * s;
double boundsHeight = (double) comp.getPreferredSize().height * s;

comp.setBounds((int)x - ((int)boundsWidth/2),(int) y - ((int)boundsHeight /2),(int) boundsWidth, (int) boundsHeight);

position.setZ(s);
z_order[p++] = position;
}

There you go, that's the math part! Not too hard was it? Basically we use a bit of trig together with the angle of the component to calculate the width and height of the component. The final angle is determined based on the current angle of the component, the overall rotation of the whole carosel (rotationalOffset, used to move components to the front or back). Once the x,y position are determined we need to know how to scale the component (they should get bigger at the front, and smaller at the back). We do this the really easy way which is to take the y co-ordinate (further down the screen it is, the bigger it should be drawn). We then scale the component's preferred size by the factor.

We also use the scale to determine the z position of the compnent (we don't want swing drawing a back-most component over the front most). We are going to sort an order these in the next step, so we record the target z before looping back to the next component.

//Now sort out the z, we may need to cache the dimensions, do the z and then reset the bounds, see what happens on redraw first
//bubble sort is actually very fast for a small number of items, and this layout shouldn't be used for loads.
boolean swaps = true;
int limit = numberOfItems-1;
while (swaps){
swaps = false;
for (int j=0;j
if (z_order[j].getZ()
CaroselPosition temp = z_order[j+1];
z_order[j+1]=z_order[j];
z_order[j]=temp;
swaps=true;
}
}
limit--;
//We must be done if we hit the bottom
if (limit==0){
swaps=false;
}
}

//Re-order everything (yet as little as possible Happy
for (int j=0;j
if (target.getComponentZOrder(z_order[j].getComponent())!=j){
target.setComponentZOrder(z_order[j].getComponent(),j);
}
}
}


A simple bubble sort is used to order the components biggest to smallest (frontmost to backmost), and then commiting it to the swing z-order. And that's it, our carosel is complete. Without worrying about the animation, there's nothing more to know. You end up with this...

Pasted Graphic 2
It's a little hard to see, but our four components are laid out in a circle...

So there's still quite a lot of code we haven't looked at, let's move on and start looking at how the animation works. A useful way of seeing what we animate, is to look at the isAnimating method. It determines if the animation timer should be stopped, by considering all of the things that may cause animation.

/**
* Determines if an animation is currently playing
*
* @return true if it is animating, false if it isn't
*/
protected boolean isAnimating(){
if (!animationTimer.isRunning()){
return false;
}

try{
for (Component comp : components) {
CaroselPosition cpos = getPosition(comp);
if (cpos.isAnimating()){
return true;
}
}
} catch (ConcurrentModificationException ex){
return isAnimating();
}

if (Math.abs(rotationalOffset - targetOffset) < 0.001){
return false;
} else {
return true;
}

}

At the start it looks at each component to see if they are in their final position (our components animate into their final position), and also looks at the overall rotationalOffset to see if it needs to carry on spinning a component to the front. So what do we do to animate? It's really very simple, the actionPerformed method is called when the timer fires, so let's see what it does.

/**
* Manages timer actions, terminating the timer if any event is fully achieved
*
* @param actionEvent the action event, although this will always be the timer
*/
public void actionPerformed(ActionEvent actionEvent) {
if (animationTimer==null){
return;
}

if (!animationTimer.isRunning()){
return;
}

if (!isAnimating()){
animationTimer.stop();
return;
}

//Update any animating icons, could be subject to modification
//outside the EDT
try {
for (Component comp : components) {
CaroselPosition cpos = getPosition(comp);

if (cpos.isAnimating()){
cpos.updateAngle();
}
}
} catch (ConcurrentModificationException cMe){
actionPerformed(actionEvent);
}
rotationalOffset += (targetOffset - rotationalOffset) / 6.0;
if (container!=null){
this.layoutContainer(container);
if (container instanceof Component){
((Component) container).repaint();
}
}
}

It looks at each component, see's if they are animating their position and updates their position if they are. In addition it also looks at the target angle for the overall carosel and closes the gap a little if it isn't there yet. If there is nothing left to animate, it stops the timer.

That's all very well, but how do we make our carosel spin something to the front? Well it's not up to our layout manager to decide if it needs to, but it should provide some methods to objects that use it so they can specifying a component to move to the front. The setFrontmostComponent method achieves this, itself using a private setTarget() method.

/**
* Sets a target angle to rotate to, always choses a direction that is less than
* or equal to 180 degrees
*
* @parm target The target angle in radians
*/
private void setTarget(double target){
//We should never have to rotate more than PI radians
while (Math.abs(target-rotationalOffset) > Math.PI){
if (target
target += Math.PI * 2;
} else {
target -= Math.PI * 2;
}
}
targetOffset = target;
if (!animationTimer.isRunning()){
animationTimer.setCoalesce(true);
animationTimer.setRepeats(true);
animationTimer.setDelay(20);
animationTimer.start();
}
}

/**
* Moves the specified component to the front
*
* @param component The component move to the front
*/
public void setFrontMostComponent(Component component){
setTarget(-getPosition(component).getTargetAngle());
}

Set front most component extracts the angle of the specified component from our list of additional component position data, and calls set target with that angle. setTarget takes the angle and determines the "shortest" route to the target angle (clockwise or anti-clockwise). It sets up the target offset, and starts the animation timer if it needs to. That's it. Pretty easy huh?

In the next blog we'll look at deriving a new component from JPanel which performs some of the other useful tasks... and oh-yes.... we'll make it look sexy too!

Note: The iTunes CD Images used in this example are reproduced here by kind permission of Brian Zeitler from PixelNet Design</