Introduction
Some time ago, I was complaining to a friend that I had no interesting projects to implement in my spare time; my friend told me to stop whining and just implement an iPhone style menu.
This article demonstrates a way to create an animated, editable icon menu similar to the one found on the iPhone home screen.
As a disclaimer, I'd like to say that this isn't an attempt to directly copy the iPhone menu; this article aims to describe some coding techniques that we can employ when animating a GUI (the iPhone root menu is far, far, far better than this implementation).
Some of the code in this article is similar to the animation code in my Implementing a smoothly animated ListBox article, but this one uses custom drawing and offscreens which are concepts that can be very useful when spicing up a GUI.
Requirements
Before I started this implementation, I decided that my menu had to fulfill the following requirements:
- Easy to operate using fingers instead of stylus.
- Ability to manually re-arrange the layout of the icons during runtime.
- Ability to manually delete icons during runtime.
I haven't implemented support for the neat paging functionality found on the iPhone where the menu extends over multiple screens, but I'm tempted to see if I can hook this menu up to my smooth listbox and achieve the same effect.
Using the Code
The Zip file contains a Visual Studio solution with two projects: one for PocketPC 2003, and one for the Windows Desktop. I find it easier and faster to test my stuff on the desktop as I don't have to wait for the emulator then.
The menu works in this way:
- Single click an icon to launch or open it.
- Click and hold to enter edit mode; edit mode is indicated by the icons wiggling and a delete icon in the upper left corner of the icons that can be deleted. Drag and release to re-arrange the icons.
- To exit edit mode, single click somewhere. If the the delete icon is clicked, the icon is deleted.
In the example implementation, only the Calculator icon has a handler attached to it, so that's the only one where a single click has an effect.
The Menu
For this project, I started out with creating a User Control for the menu. This control is responsible for hosting the icons and animating them, but it is not responsible for the actual position of each icon, that is up to the Icon
class. The IconMenu
class only tells its icons where they probably should be, it's up to the icons to actually render themselves in that position if they want.
Align to Grid
On the iPhone, the icons align themselves to a neat grid, therefore my IconMenu
class contains a list of Slot
s, which is a class that has a Rectangle
defining its bounds and possibly a reference to an icon. The IconMenu
has a list of Slot
s rather than a two dimensional array of Slot
s as it re-arranges the slots when the screen is tilted.
public class Slot
{
private Rectangle bounds;
private Icon icon;
public Slot(Rectangle bounds)
{
this.bounds = bounds;
}
public bool IsEmpty
{
get { return icon == null; }
}
public Icon Icon
{
get { return icon; }
set { icon = value; }
}
public Rectangle Bounds
{
get { return bounds; }
}
}
The IconMenu Class
The IconMenu
class has four tasks as its main responsibility:
- Own the icons and provide the preferred layout for them
- Handle animation
- Handle mouse/stylus input
- Fire events to client code
Icon Ownership
By maintaining a list of Icon
s as well as a list of Slot
s, the IconMenu
can assign each Icon
to a Slot
, and since the Slot
s have layout information stored as a Rectangle
, the IconMenu
will have the preferred position for each icon as soon as it's assigned to a Slot
.
The Icon
s also keep track of the Slot
they belong to in order to be able to get their preferred position when they need to be rendered to screen.
The Icon
s and Slot
s are assigned in the AssignSlotToIcons
method:
private void AssignSlotToIcons()
{
foreach (Slot slot in slots)
{
slot.Icon = null;
}
foreach (Icon icon in icons)
{
icon.Slot = null;
foreach (Slot slot in slots)
{
if (slot.IsEmpty)
{
icon.Slot = slot;
slot.Icon = icon;
break;
}
}
}
}
This is done whenever the IconMenu
needs to re-arrange the Icon
s, which in turn is whenever the IconMenu
size changes or an Icon
is deleted.
The Relayout
method takes care of rearranging the Slot
s:
private void Relayout()
{
offscreen = null;
int numberOfHorizontalSlots = Width / slotWidth;
int numberOfVerticalSlots = Height / slotHeight;
int horizontalPadding = (Width % slotWidth) / 2;
int verticalPadding = (Height % slotHeight) / 2;
icons.Sort(delegate(Icon left, Icon right)
{
if (left.Slot == null)
return 1;
else
{
if (right.Slot == null)
return -1;
}
return slots.IndexOf(left.Slot) - slots.IndexOf(right.Slot);
});
slots = new List<slot>();
for (int i = 0; verticalPadding + (i + 1) * slotHeight < Height; ++i)
{
for (int j = 0; j < numberOfHorizontalSlots; ++j)
{
slots.Add(
new Slot(
new Rectangle(horizontalPadding + j * slotWidth,
verticalPadding + i * slotHeight,
slotWidth,
slotHeight)));
}
}
AssignSlotToIcons();
Invalidate();
}
This way, the Icon
s will always (almost) have a Slot
where they belong; the exception to this rule is when the layout of the icons are manually changed during runtime by the user. In that scenario, it's the icon's responsibility to ignore its assigned slot and render itself where the stylus/mouse is.
Rendering
In order to improve performance, I decided to go for custom rendering, and it's convenient as it also gives me more control over how to draw the control.
To reduce flicker, I used an "offscreen"; this is a concept where all the UI elements are first rendered to a non-visible memory buffer, and then the contents of that buffer are copied onto the screen memory. As the copy operation is fast, this prevents any half drawn controls to be momentarily displayed to the user. And, that reduces or removes the flicker.
The offscreen is actually just a Graphics
object from an Image
, and the Image is recreated to the size of the IconMenu
whenever the menu's size changes.
public class IconMenu : UserControl
{
...
private Image offscreenImage = null;
private Graphics offscreen = null;
private void CreateOffscreen()
{
offscreenImage = new Bitmap(Math.Max(Width, 1), Math.Max(Height, 1));
offscreen = Graphics.FromImage(offscreenImage);
}
...
}
Rendering is done in OnPaintBackground
by first filling the entire offscreen with the background color and then requesting all Icon
s, except the currently selected one, to render themselves onto the offscreen. The reason the selected one is exempted is so that it can be rendered last and thus be rendered on top of all the others as it would be annoying to have the current control partly or completely obscured by another control.
protected override void OnPaintBackground(PaintEventArgs e)
{
if (offscreen == null)
{
CreateOffscreen();
}
offscreen.FillRectangle(new SolidBrush(BackColor), ClientRectangle);
foreach (Icon icon in icons)
{
if (!icon.IsMouseControlled)
{
icon.Paint(offscreen, state == State.Edit, deleteMarker);
}
}
if (selectedIcon != null)
{
selectedIcon.Paint(offscreen, state == State.Edit, deleteMarker);
}
e.Graphics.DrawImage(offscreenImage, 0, 0);
}
The Icon Class
The Icon
class is responsible for the visual representation of an icon, its position, and for maintaining the delegates that should be called if the icon is clicked.
In order to get the Icon
s to move around to a new slot when a user is manually re-arranging the icons, I decided to store two positions: the actual location and the desired location. That means that if a user drags the selected Icon
into another Icon
's Slot
, I only need to set a new desired location for the Icon
that needs to move out of the way to give room to the selected Icon
. The Icon
can figure out itself how to get there and at what speed.
public class Icon
{
...
private Vector location = new Vector();
private Vector desiredLocation = new Vector();
...
public void UpdateOnMouseLocation(Point mouseLocation)
{
desiredLocation = (Vector)mouseLocation;
desiredLocation.X -= image.Width >> 1;
desiredLocation.Y -= image.Height >> 1;
}
}
Animating the Icon
The Icon
's animation method re-calculates the Icon
's actual position, and it considers two things:
- Is the
Icon
currently being dragged (mouseControlled
is true
)?
- Is the
IconMenu
being manually re-arranged by the user?
If the Icon
is being dragged, then its location is always set to its desired location, which is the location of the mouse/stylus.
If the IconMenu
is being re-arranged, then a random offset is added to make the Icon
s wobble around a bit. Unfortunately, I can't easily apply a rotational transformation like on the iPhone, so I settled for just having the icons wiggle a bit instead. It doesn't look as good as on the iPhone, but it's good enough for this article I think.
public void Animate(float elapsedTime, bool inEditState)
{
if (mouseControlled)
{
location = desiredLocation;
}
else
{
if (slot != null)
{
int x = slot.Bounds.Location.X +
((slot.Bounds.Width - size.Width) >> 1);
int y = slot.Bounds.Location.Y +
((slot.Bounds.Height - size.Height) >> 1);
if (inEditState)
{
x += random.Next(3, 6) * (random.Next(10) > 5 ? -1 : 1);
y += random.Next(3, 6) * (random.Next(10) > 5 ? -1 : 1);
}
desiredLocation.Set(x, y);
}
Vector targetVector = desiredLocation - location;
float velocity = targetVector.Length;
targetVector.Normalize();
location += targetVector * velocity * elapsedTime;
}
}
Rendering of the Icon
Rendering of the Icon
takes care of rendering the Icon
at its desired location as well as rendering the delete icon in the upper left corner in edit mode.
private void DrawImage(Graphics graphics, Image image,
Point point, int width, int height)
{
graphics.DrawImage(image, new Rectangle(point.X, point.Y, width, height),
0, 0, width, height,
GraphicsUnit.Pixel, imageAttributes);
}
public void Paint(Graphics graphics, bool inEditState, Image deleteMarker)
{
Point point = (Point)location;
DrawImage(graphics, image, point, size.Width, size.Height);
if (!readOnly && inEditState && deleteMarker != null)
{
DrawImage(graphics, deleteMarker, point,
deleteMarker.Width, deleteMarker.Height);
}
}
This image shows the icon menu in edit state:
Handling Input
As with my smooth list box, mouse down, move, and up events are handled to figure out what has been clicked or dragged. The first of these, mouse down, is simple enough:
private void HandleMouseDown(object sender, MouseEventArgs e)
{
mouseIsDown = true;
mouseDownTimestamp = DateTime.Now;
mouseDownPosition = new Vector(e.X, e.Y);
Icon icon = GetIconAtPoint((Point)mouseDownPosition);
if (icon != null)
{
selectedIcon = icon;
}
}
Simply record the time stamp so that we can figure out if this is a click or hold, and get the icon under the mouse/stylus (if any). This is done using one of two helper methods:
private Slot GetSlotAtPoint(Point point)
{
foreach(Slot slot in slots)
{
if (slot.Bounds.Contains(point))
{
return slot;
}
}
return null;
}
private Icon GetIconAtPoint(Point point)
{
foreach (Icon icon in icons)
{
if (icon.Slot.Bounds.Contains(point))
{
return icon;
}
}
return null;
}
The other method, GetSlotAtPoint
, is, of course, used to get the slot under the mouse/stylus.
The second event, mouse move, is slightly more complicated as there are more things to handle if a mouse is selected (as detected in HandleMouseDown
).
void HandleMouseMove(object sender, MouseEventArgs e)
{
if (selectedIcon != null)
{
if (state == State.Edit)
{
Point mouseLocation = new Point(e.X, e.Y);
selectedIcon.UpdateOnMouseLocation(mouseLocation);
Slot slot = GetSlotAtPoint(mouseLocation);
if (slot != null)
{
int selectedIndex = slots.IndexOf(selectedIcon.Slot);
int currentIndex = slots.IndexOf(slot);
if (selectedIndex != currentIndex)
{
for (int i = selectedIndex;
i != currentIndex;
i += Math.Sign(currentIndex - selectedIndex))
{
Slot slotA = slots[i];
Slot slotB = slots[i + Math.Sign(currentIndex - selectedIndex)];
slotA.Icon = slotB.Icon;
if (slotA.Icon != null)
{
slotA.Icon.Slot = slotA;
}
}
slot.Icon = selectedIcon;
selectedIcon.Slot = slot;
}
}
}
else
{
if (DateTime.Now - mouseDownTimestamp >= clickDelay)
{
state = State.Edit;
selectedIcon.IsMouseControlled = true;
}
}
}
}
Last part, mouse up, also has a few things to do:
private void HandleMouseUp(object sender, MouseEventArgs e)
{
mouseIsDown = false;
if (selectedIcon != null)
{
if (DateTime.Now - mouseDownTimestamp < clickDelay)
{
switch (state)
{
case State.Launch:
selectedIcon.OnLaunch();
break;
case State.Edit:
Slot slot = GetSlotAtPoint((Point)mouseDownPosition);
if (deleteMarker != null &&
slot != null &&
!slot.IsEmpty &&
!slot.Icon.IsReadOnly)
{
Vector slotPosition = (Vector)slot.Icon.Location;
Vector positionInSlot = mouseDownPosition - slotPosition;
if (positionInSlot.X < deleteMarker.Width &&
positionInSlot.Y < deleteMarker.Height)
{
DialogResult result =
MessageBox.Show(
"Are you sure you want to delete this?",
"Delete",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button2);
if (result == DialogResult.OK)
{
icons.Remove(slot.Icon);
OnDeleted(slot.Icon);
slot.Icon = null;
Relayout();
}
break;
}
}
state = State.Launch;
selectedIcon.IsMouseControlled = false;
break;
}
}
else
{
int selectedIndex = slots.IndexOf(selectedIcon.Slot);
if (selectedIndex > icons.Count - 1)
{
selectedIcon.Slot.Icon = null;
Slot slot = slots[icons.Count - 1];
slot.Icon = selectedIcon;
selectedIcon.Slot = slot;
}
}
selectedIcon.IsMouseControlled = false;
selectedIcon = null;
}
else
{
if (state == State.Edit)
{
state = State.Launch;
}
}
}
Missing Stuff
Like I stated in the Introduction, this isn't intended to be a complete implementation, it's just to show how this type of functionality can be achieved. Because of this, there are quite a few shortcomings in this implementation:
- The animation should be paused when the icons are not visible to save CPU cycles.
- The icons should have a defined pattern for moving offscreen and back on when an icon is launched.
- There's a bug that will cause the list to break if there are more icons than slots.
Sorry about these.
As always, any comments and suggestions are most welcome.
History
- 2009-04-21: First version.