I've also tweaked the carousel example following a suggestion from Romain Guy (his site has moved btw, follow updated link) that the jittering some people experienced is due to single pixel increments in size, so I've made it only size to even pixels and we'll see if that helps (Webstart updated).
Other bits of house keeping have also been done to help people find their way around the site, so I hope it's a little bit clearer now.
Finally, thanks so much for all of the messages (and comments) with encouragement and ideas. Every single one has been valuable and greatly appreciated.
Starting off, let's list out the tasks ahead of us:
1. We need to do a small amount of re-factoring on
the carousel layout to allow us to sub-class it and
over-ride a couple of pieces of functionality.
2. We need a new component that will contain a
carousel, and a menu (I'm going to use a JList, it
does the job!)
3. The menu needs a custom cell renderer to make the
selected cell look pretty
The re-factoring was pretty minimal, but I did add a
couple of methods, just before I start that, I need
to mention one of the existing methods which needed
to be over-ridden:
public void setFrontMostComponent(Component
component)
Previously this would spin the
specified component to the FRONT of the carousel.
This time, I wanted it to be on the right of the
carousel, so the new method (in the rather
imaginatively named OffsetCarouselLayout class). This
function just determines the target angle
differently.
protected boolean shouldHide(Component comp,
double angle)
One of the problems with the
over-riding of the above function is that you can
have component in front of the currently focused one.
This new function is used to determine if a component
should be hidden (based on where it is on the
carousel). It lets me stop things appearing in front
of the current component.
The next two are pretty obvious, they let me
manipulate the center of the carousel (in the
component) and the radius of the carousel (I didn't
want it quite as squashed).
That's it, not bad, gotta-love OO programming!
So, let's summarize what we have; a single component
with a JCarousel component (with a new layout
manager), and a JList in it, pretty straight
forward....
As you can imagine, Java makes most of this pretty
easy. But there are a couple more steps that you'll
find in the source code.
It turns out, JLists don't like wrapping the selected
object when the user is using the keys to cycle
through (it gets stuck at the top or the bottom). So
you'll see in the code of the Carousel Menu that it
has a keyboard listener to move the cursor to the top
or the bottom when it hits one of the ends. Also we
need to update which component is at the "front" of
the carousel based on which item is selected, and a
simple implementation of the list selection listener
achieves that.
And of course, we need a nice border around the
selected item. I've cheated a little and created a
simple border called "ImageBorder" which takes a
bounded image (like the one shown below) and a set of
insets, and slices and dices it to make it
resize-able.
I've added the code for this below, and of course,
once I'm back from my trip I'll update my project
file so that you can all download the source. And
rather wonderfully, that's it. Once again, Java can
really deliver on a modern UI and I think you'll
agree it makes my blog examples a little more usable!
![]()
The final implementation essentially uses a
GridBagLayout to control the dock, with spacers
"pushing" the icons in the dock into the center. The
mouse is tracked, and when it moves over one of the
Components (you can just add any Java component into
there) its preferred size is changed (therefore
increasing the size). Of course, like everything it's
not QUITE that simple. The diagram below shows the
basic strategy.
I'll update the source code for the Blog project
when I get back home after my trip, but I'd like to
focus on a couple of the interesting problems that
needed solving.
Scaling The Icons Based on Distance From
Pointer
The DockPanel sets the preferred size
of the icons to control their "zooming" in the dock.
It's implemented in a method called. This is
implemented in the getComponentPreferedSize method.
First it checks to see if the mouse is actually
hovering over the given icon, if it is, it returns
the enlarged size, otherwise it looks for the
distance from the mouse pointer to the center of the
icon, and linearly scales. The method is protected so
it could be over-ridden by a subsequent
implementation. I point this out because I'm pretty
sure Apple are using a Sigma curve or the like scale
theirs as the distance increases. I'll leave it to
you to try it yourself!
Watching for Change
Another over-used class of mine that I've added to
the Blog project is a MouseTracker which does a
couple of things, including tracking a mouse inside a
container, filtering out a lot of the events, and
just passing interesting ones across (when I publish
the source you'll see). One of the
MouseTrackerListener call-backs is mouseMoved, and
it's here that I update the parameters on the
GridBagLayout to make any changes. There's one other
place that the same thing is done. As usual we don't
want everything flicking from small to giant etc so I
have a timer that updates a tweened animation between
the target size for the icons in the dock, and the
size they currently are. Consequently actionPerformed
also updates the layout.
Easy Animation
I've been pretty lazy here. If you look at the
updateLayout method (which updates all the grid bag
constraints) you'll see that the result of
getComponentPreferredSize is passed into the
tweenValue method. This is where the animation is
done. The tweenValue takes a current size, and a
target size and calculates a value between the two
sizes. If the two aren't already the same it lets the
animation timer know that the size being passed on
isn't the final size, and then returns an
intermediate size. Finally, it does a little bit of
"clipping" to make sure that the icons can always fit
in the space available.
Docking to the Different Sides
The code looks pretty lengthy, but a
lot is taken up by dealing with the various layout
wrinkles that come along with aligning the dock to
different sides, so as you read through, just pick a
compass point and worry about that.
Now speaking of the source code here it is, you can
launch the web-start demo (it creates an AWT has been
updated with a dock tab too to keep you going until
all the source is available.
Note: I have fixed the problem that
stopped the web-start working (it uses an
AWTEventListener which requires ALL permissions, so
the jar needed to be signed).
Dock Panel Source Code
/*
* DockPanel.java
*
* Created on January 9, 2007, 6:22 AM
*
* Re-implementation of the dock to use a
grid-bag-laid-out panel instead
* of the very complicated code I was developing.
*
*/
package com.blogofbug.swing.components;
import com.blogofbug.swing.delegates.MouseTracker;
import
com.blogofbug.swing.delegates.MouseTrackerListener;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.Hashtable;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import java.awt.Insets;
/**
*
* @author nigel
*/
public class DockPanel extends JPanel implements
MouseTrackerListener, ActionListener{
/**
* Enumeration object to contain the side of the
screen/window the dock is against
*
*/
public enum Side {NORTH, SOUTH, EAST, WEST};
//The enlarged size of the icons
private int enlargedSize = 96;
//The normal size of the icons
private int normalSize = 32;
//Insets around each dock icon
private int insets = 2;
//The side the dock is docked to
private Side dockedTo = Side.SOUTH;
//Autohide?
private boolean autoHide = false;
//Auto-hiding!
private boolean hiding = false;
//Layout and standard constraints
private GridBagLayout layout = new GridBagLayout();
private GridBagConstraints filler = new
GridBagConstraints();
private GridBagConstraints title = new
GridBagConstraints();
private GridBagConstraints icon = new
GridBagConstraints();
//Spacer Panels
protected JSpacer spacer; ;
protected JSpacer firstSpacer; ;
protected JSpacer lastSpacer;
//Hold index of components and their titles
private Hashtable iconLabels = new Hashtable();
//Track mouse movements (will be over the entire
component)
private MouseTracker mouseTracker;
//Animation timer
private Timer timer = new Timer(0,this);
private boolean animating = false;
//Mouse location (last known)
private Point lastKnownMouse = new Point(0,0);
/** Creates a new instance of DockPanel */
public DockPanel(int normalSize, int enlargedSize,
Side dockedTo) {
//Set up the basics
this.dockedTo = dockedTo;
this.enlargedSize = enlargedSize;
this.normalSize = normalSize;
//Prepare the timer
timer.setCoalesce(true);
timer.setDelay(20);
timer.setRepeats(true);
timer.start();
//make me transparent
setBackground(null);
setOpaque(false);
//Set up the mouse tracker
mouseTracker = new MouseTracker(this,this);
//Set up the spacers
spacer = new JSpacer();
firstSpacer = new JSpacer();
lastSpacer = new JSpacer();
//Initial setup
setLayout(layout);
initializeLayout();
add(spacer,filler);
add(firstSpacer,icon);
add(lastSpacer,icon);
//Perform initial layout
updateLayout(null);
}
public boolean isAutoHiding(){
return autoHide;
}
public void setAutoHiding(boolean autoHide){
this.autoHide = autoHide;
mouseMoved(lastKnownMouse);
}
public DockPanel(int normalSize, int enlargedSize){
this(normalSize,enlargedSize,Side.SOUTH);
}
public void setNormalSize(int normalSize){
this.normalSize = normalSize;
updateLayout(null);
layout.layoutContainer(this);
}
public void setEnlargedSize(int enlargedSize){
this.enlargedSize = enlargedSize;
updateLayout(null);
layout.layoutContainer(this);
}
/**
* Sets up the basic layout and then calls
updateLayout to finalize the
* individual dock elements
*/
private void initializeLayout(){
//Standard settings for the filler panel
filler.fill = GridBagConstraints.BOTH;
filler.weightx = 1.0;
filler.weighty = 1.0;
//Standard settings for the title components
title.anchor = GridBagConstraints.CENTER;
//Standard settings for the icon components
icon.anchor = GridBagConstraints.CENTER;
icon.weightx = 0.0;
icon.weighty = 0.0;
icon.insets = new
Insets(insets/2,insets/2,insets/2,insets/2);
//Do various bits of specific constraints layout
switch (dockedTo){
case SOUTH:
filler.gridx = 0;
filler.gridy = 0;
filler.gridheight =1;
filler.gridwidth = GridBagConstraints.REMAINDER;
title.gridx = 0;
title.gridy = 1;
icon.gridx = 0;
icon.gridy = 2;
icon.anchor = GridBagConstraints.SOUTH;
break;
case NORTH:
filler.gridx = 0;
filler.gridy = 2;
filler.gridheight =1;
filler.gridwidth = GridBagConstraints.REMAINDER;
title.gridx = 0;
title.gridy = 1;
icon.gridx = 0;
icon.gridy = 0;
icon.anchor = GridBagConstraints.NORTH;
break;
case WEST:
filler.gridx = 2;
filler.gridy = 0;
filler.gridheight =GridBagConstraints.REMAINDER;
filler.gridwidth = 1;
title.gridx = 1;
title.gridy = 0;
icon.gridx = 0;
icon.gridy = 0;
icon.anchor = GridBagConstraints.WEST;
break;
case EAST:
filler.gridx = 0;
filler.gridy = 0;
filler.gridheight =GridBagConstraints.REMAINDER;
filler.gridwidth = 1;
title.gridx = 1;
title.gridy = 0;
icon.gridx = 2;
icon.gridy = 0;
icon.anchor = GridBagConstraints.EAST;
break;
}
}
/**
* Changes the side of the panel the dock is attached
to
*/
public void setDockedTo(DockPanel.Side dockedTo) {
this.dockedTo = dockedTo;
initializeLayout();
layout.setConstraints(spacer,filler);
updateLayout(null);
layout.layoutContainer(this);
}
/**
* Determines the appropriate gridbag weight for the
"book-end" spacers
* at either end of the dock
*
* @return The weight
*/
private double spacerWeightX(){
switch (dockedTo){
case NORTH:
case SOUTH:
return 1.0;
case EAST:
case WEST:
return 0.0;
}
return 0.0;
}
/**
* Determines the appropriate gridbag weight for the
"book-end" spacers
* at either end of the dock
*
* @return The weight
*/
private double spacerWeightY(){
return 1.0-spacerWeightX();
}
/**
* Determines the appropriate size for the component
based on the current "mouse over"
* component, the location in the dock area relative
to the overall dock panel, and a given component
* This is protected so it can be over-ridden to apply
a better alogrithm .
*
* @param p The location of the mouse relative to the
overall dock
* @param compoment The component to be sized
* @param highlightedComponent The current component
with the mouse over it
* @return A Dimension object with the recommened size
of the component
*/
protected Dimension getComponentPreferedSize(Point p,
Component component, Component highlightedComponent){
//If we are hiding it should be small
if (hiding){
return new Dimension(insets*3,insets*3);
}
//determine it's distance based on half the size of
the biggest component
double compSize = component.getSize().height;
double distanceFromCenter = 0;
double delta = (double) (enlargedSize-normalSize);
if ((highlightedComponent!=null)){
if (highlightedComponent==component){
return new Dimension(enlargedSize,enlargedSize);
}
switch (dockedTo){
case NORTH:
case SOUTH:
distanceFromCenter = Math.abs((double)
((component.getLocation().x + compSize/2 )- p.x));
break;
case EAST:
case WEST:
distanceFromCenter = Math.abs((double)
((component.getLocation().y + compSize/2 )- p.y));
break;
}
double cdy = 1.0 - distanceFromCenter/(double)
(enlargedSize*2);
double tSize = Math.max((double) normalSize + cdy *
delta,(double) normalSize);
return new Dimension((int)tSize,(int) tSize);
} else {
return new Dimension(normalSize,normalSize);
}
}
/**
* Essentially removes the spacer that forces the dock
to the bottom of the
* panel, neat if you would like to use the panel as
something like a
* special tab top.
*
* @param fillSpace true if the panel should fill up
any empty space above the dock,
* false if it should not
*/
public void fillSpace(boolean fillSpace){
spacer.setVisible(fillSpace);
}
/**
* Determines a value between the current and target
including easing. Can
* be over-ridden if desired to have different sizing
behavior. Implementers
* should note that if the size is not clipped to
ensure that the icons will
* all fit in the dock, gridbag does Bad Things (tm).
*
* @param current The current value
* @param target The target value
* @return A value equal to or between target and
current
*/
protected double tweenValue(double current, double
target){
//Just make sure we do nothing if they are the same
if (current==target){
return target;
}
//Determine the difference
double delta = (target-current)/4.0;
//Tween in the right direction, but always by at
least one pixel
if (delta>0){
delta = Math.max(1.0,delta);
} else if (delta<0){
delta = Math.min(-1.0,delta);
}
switch (dockedTo){
case SOUTH:
case NORTH:
return Math.min(current+delta,(
getWidth()-insets*iconLabels.size()*2)/iconLabels.size()-10);
default:
return
Math.min(current+delta,(getHeight()-insets*iconLabels.size()*2)/iconLabels.size());
}
}
/**
* Determines if a component is in the dock
*
*/
public boolean dockContains(Component component){
if (iconLabels.get(component)!=null){
return true;
}
return false;
}
/**
* Takes a current size, and determines a new size
setting up an animation
* if needed.
*
* @param currentSize The current size of the
component
* @param newSize The new size of the component
* @return The in-between size
*/
private Dimension tweenSize(Dimension currentSize,
Dimension newSize){
if ((newSize.width != currentSize.width) ||
(newSize.height != currentSize.height)){
animating = true;
newSize.width = (int)
tweenValue(currentSize.width,newSize.width);
newSize.height = (int)
tweenValue(currentSize.height,newSize.height);
}
return newSize;
}
/**
* Determines the new settings for the gridbag layout,
and then applies them
* to all components
*
*/
private void updateLayout(Point p){
Component highlightedComponent = null;
animating = false;
if (p!=null){
highlightedComponent = getComponentAt(p);
//Make sure it's at least in the dock zone
switch (dockedTo){
case NORTH:
if (p.y>spacer.getY()){
highlightedComponent=null;
}
break;
case SOUTH:
if (p.y
highlightedComponent=null;
}
break;
case WEST:
if (p.x>spacer.getX()){
highlightedComponent=null;
}
break;
case EAST:
if (p.x
highlightedComponent=null;
}
break;
}
if ((highlightedComponent==firstSpacer) ||
(highlightedComponent==lastSpacer)){
highlightedComponent = null;
}
}
//Variables used for incrementing grid-x's and
grid-y's
int dx=0,dy=0;
switch (dockedTo){
case NORTH:
case SOUTH:
dx=1;
break;
case EAST:
case WEST:
dy=1;
break;
}
//Set everything up as it should be
initializeLayout();
//Add first spacer and move to next position
icon.weightx = spacerWeightX();
icon.weighty = spacerWeightY();
icon.fill = GridBagConstraints.BOTH;
layout.setConstraints(firstSpacer,icon);
icon.fill = GridBagConstraints.NONE;
icon.gridx += dx;
icon.gridy += dy;
title.gridx += dx;
title.gridy += dy;
//Iterate through the components updating everything
Component[] components = getComponents();
for (Component component : components){
//Make sure it's not a filler or a title
if (!((component == spacer) || (component instanceof
DockLabel) || (component == firstSpacer) ||
(component == lastSpacer))){
//Set the weight
icon.weightx = 0.0;
icon.weighty = 0.0;
//Icon first
layout.setConstraints(component, icon);
//Set the prefered size of the component
component.setPreferredSize(tweenSize(component.getSize(),getComponentPreferedSize(p,component,highlightedComponent)));
//Set the visibility of its label
if (highlightedComponent==component){
iconLabels.get(component).setVisible(true);
} else {
iconLabels.get(component).setVisible(false);
}
//Title next
layout.setConstraints(iconLabels.get(component),title);
//Move to next position
icon.gridx += dx;
icon.gridy += dy;
title.gridx += dx;
title.gridy += dy;
}
}
//Add the last spacer
icon.weightx = spacerWeightX();
icon.weighty = spacerWeightY();
icon.fill = GridBagConstraints.BOTH;
layout.setConstraints(lastSpacer,icon);
}
/**
* Adds a new item to the dock
*
*/
public void addDockElement(Component component,
String label){
if (iconLabels.get(component)!=null){
return;
}
DockLabel newLabel = new DockLabel(label);
//We would like it to do something funcky as it adds
them and make them grow
component.setPreferredSize(new Dimension(0,0));
iconLabels.put(component,newLabel);
add(component);
add(newLabel);
updateLayout(mouseTracker.getPosition());
}
/**
* Not interested
*/
public void mouseCrossThreshold(boolean mouseEntered)
{
}
/**
* When the mouse moves, update the layout. Should be
optimized when the
* highlighted component is the spacer to not do
anything
*/
public void mouseMoved(Point position) {
if (autoHide){
Component mouseOver = getComponentAt(position);
if ((mouseOver == firstSpacer) || (mouseOver ==
lastSpacer) || (spacer==mouseOver)){
hiding = true;
} else {
hiding = false;
}
} else {
hiding = false;
}
lastKnownMouse = position;
updateLayout(position);
layout.layoutContainer(this);
}
public void paint(Graphics graphics) {
graphics.setColor(new Color(200,200,220,127));
Rectangle dockSize = null;
int oldNormalSize=normalSize;
if (hiding){
normalSize = Math.min(normalSize,
((dockedTo==Side.NORTH) || (dockedTo==Side.SOUTH)) ?
firstSpacer.getHeight(): firstSpacer.getWidth());
}
//Draw the main dock background
switch (dockedTo){
case NORTH:
dockSize = new
Rectangle(firstSpacer.getX()+firstSpacer.getWidth(),0,getWidth()-(lastSpacer.getWidth()+firstSpacer.getWidth()),normalSize);
break;
case SOUTH:
dockSize = new
Rectangle(firstSpacer.getX()+firstSpacer.getWidth(),getHeight()-normalSize,getWidth()-(lastSpacer.getWidth()+firstSpacer.getWidth()),normalSize);
break;
case WEST:
dockSize = new
Rectangle(0,firstSpacer.getHeight(),normalSize,getHeight()-(lastSpacer.getHeight()+firstSpacer.getHeight()));
break;
case EAST:
dockSize = new
Rectangle(getWidth()-normalSize,firstSpacer.getHeight(),normalSize,getHeight()-(lastSpacer.getHeight()+firstSpacer.getHeight()));
break;
}
if (hiding){
normalSize = oldNormalSize;
}
dockSize.x-=insets;
dockSize.y-=insets;
dockSize.width+=insets*2;
dockSize.height+=insets*2;
graphics.fillRect(dockSize.x,dockSize.y,dockSize.width,dockSize.height);
//Draw the outline
graphics.setColor(new Color(255,255,255,127));
switch (dockedTo){
case SOUTH:
graphics.drawRect(dockSize.x,dockSize.y,dockSize.width-1,dockSize.height);
break;
}
super.paint(graphics);
}
public void actionPerformed(ActionEvent actionEvent)
{
if (animating){
updateLayout(lastKnownMouse);
layout.layoutContainer(this);
}
}
//For convenience
public class JSpacer extends JComponent {
public JSpacer(){
setBackground(null);
setOpaque(false);
}
}
//Temporary until I do something else
public class DockLabel extends StrokedLabel {
public DockLabel(String title){
super(title);
}
};
}
Anyhow, there is something coming along, and here's a little teaser....

Sleep patterns have been knocked all over the place
with the return to work and the bustle of the new
year. Oh, and there's a matter of the Ashes which is
on during the middle of the night giving me an excuse
to get out of bed rather than trying to go to sleep.
So I decided to use the early hours to optimize.
A few notes first:
1. This has been optimized for dual-or-more-core
processors (more later)
2. I'm not claiming it's the fastest or best or
anything. This is intellectual mastication (ahem) and
hopefully a hint of "oh shiny!"
About 1. As I've said, once upon a time I used to
write games, back in the days when 20Mhz was pretty
exciting and SVGA was bleeding edge. I've built up
some opinions about how I would go about making use
of dual cores, and this demo implements some of them.
The simulation runs in one thread, and is considered
more important than drawing. The second thread does
the painting (javax.swing.Timer anyone) and run's
every 20th of a second (here in the UK on the Amiga,
50Hz was the frame-rate we were aiming for, so that's
what I was going for).
I have a MacBook Pro and an
almost-exactly-the-same-spec Dell 620 to test on so
they are both Dual-Core, I'd love to read your
comments on performance on single core (in fact
whatever machine you have) machines. But that brings
me to something else I really need your help with.
I'm running on Java SE 6 on both the Mac and Windows.
Windows has always been a bit faster (since I've been
writing this), but when I split up the main thread,
Windows suddenly started to claim it was A LOT
faster. As in it's claiming 0ms on the Dell for the
simulation, and often 0ms for the re-draw. Hum. Can't
see why the instrumentation would suddenly change,
but hot-damn it does seem to be running without slow
down (on the dell at least) barely chewing any (or
either) of the cores. So again, any comments on your
experiences would be great.
The web-startable app below needs Java 5, but please
try on Java 6. Please post your results... and don't
forget to click on "Tap On" when you are ready to
start the water!
![]()
As you can see, 400 drops is no longer a problem inside the "20ms" limit required to meet the Amiga's performance, in-fact you can crank it up to over 100,000 on my MBP before it starts to sweat. No great surprise there, the processor is massively faster. What has surprised me is how easy it is to implement in Java, how well Java2D works. In addition, the Java implementation is doing a whole lot more than the original version. I'm not sure if I'm going to post the full source or not, but here are some of the problems that have been solved with the old algorithm.
Vertical Columns
The old routine didn't have any idea of the inertia a particle of water had as it flowed over the scene, so when the water could go down it would, creating very artificial looking columns of falling water.
Easy to address, give the water some inertia, immediately much nicer diffusing water falls.
Self-Leveling
This was a big one, the levels of the game had to be very carefully designed to ensure that no part of a level would highlight the fact that the water would not self-level (pressure equalizing the water level). It was a real pain, and subtly caused other minor problems. Have a look at the U-bends in the screen shot below. The water should be pushing down equalizing the pressure in the u-bend.
Well in the spirit of the original code (that is that it looks as much like water as possible, without ever really simulating what water does), I've managed to solve this problem too, although it is currently impacting performance (3-4 times slower in regions where water self-levels) but there are lots of ways that can be improved it just the raw solution at the moment, plugged into the inner loop. As you can see, it's working perfectly...
So what's the point of the teasing post? Well I'm not sure what I'm going to do with this code yet, it's partly an exercise for walking down memory lane for me, in which case I'll just post the code so people can do a better job than me! However, it's also an interesting look at writing dynamic graphics algorithms in Java, in which case I'm going to publish my findings so that they can be exploited by others. Anyhow, let me know if you find it interesting, I'll make it web-startable soon so you can all play with the routine (it's kind of hypnotic).
Update: The web-start demo has been implemented, and I would really like your help with getting some results from a range of machines: Click Here