Getting Started
Here you will find both basic and advanced tutorials to help you get started using Piccolo2D. All tutorials provide examples in both Java and C#. This section assumes you have read Piccolo2D Patterns and have a basic understanding of the concepts presented there.Fisheye Calendar
This tutorial will illustrate how you might build a tabular fisheye interface using Piccolo2D. Clicking on one cell will give that cell the fisheye focus while shrinking the surrounding cells. Such an interface might be useful when you don't have much screen real estate to deal with.
Download the complete code sample in Java or
C#.
Play with the interface.
Overall Architecture
In this example, we will create two custom nodes, a
DayNode
and a
CalendarNode
. The DayNode
will be responsible for rendering the
contents of a single cell within the calendar. This node will use semantic zooming
to control how a day is rendered, depending on whether or not that day is focused.The CalendarNode
will be responsible for laying out all of the
DayNodes
in a tabular fashion. Every DayNode
will be added as a
child to the CalendarNode
. This node will also handle the user interaction,
focusing on a cell in response to a click, and animating transitions when requested to do
so.
We will then create a reusable TabularFisheye
component that extends
PCanvas
. This component will add the calendar node to the scene-graph and
resize it when the component is resized. Finally, we will create a wrapper window called
TabularFisheyeTester
and add our new component to the window.
1. Create a Day Node
We will create a node that will be responsible for rendering the contents of a single cell within in the calendar. This node will use semantic zooming to control how each day is rendered depending on whether or not the day has the fisheye focus. The day that is expanded both vertically and horizontally has the focus.
-
Here, we will create the day node. Add the following class to your project.
static class DayNode extends PNode { boolean hasWidthFocus; boolean hasHeightFocus; ArrayList lines; int week; int day; String dayOfMonthString; public DayNode(int week, int day) { lines = new ArrayList(); lines.add("7:00 AM Walk the dog."); lines.add("9:30 AM Meet John for Breakfast."); lines.add("12:00 PM Lunch with Peter."); lines.add("3:00 PM Research Demo."); lines.add("6:00 PM Pickup Sarah from gymnastics."); lines.add("7:00 PM Pickup Tommy from karate."); this.week = week; this.day = day; this.dayOfMonthString = Integer.toString((week * 7) + day); setPaint(Color.BLACK); } public int getWeek() { return week; } public int getDay() { return day; } public boolean hasHeightFocus() { return hasHeightFocus; } public void setHasHeightFocus(boolean hasHeightFocus) { this.hasHeightFocus = hasHeightFocus; } public boolean hasWidthFocus() { return hasWidthFocus; } public void setHasWidthFocus(boolean hasWidthFocus) { this.hasWidthFocus = hasWidthFocus; } protected void paint(PPaintContext paintContext) { Graphics2D g2 = paintContext.getGraphics(); g2.setPaint(getPaint()); g2.draw(getBoundsReference()); g2.setFont(CalendarNode.DEFAULT_FONT); float y = (float) getY() + CalendarNode.TEXT_Y_OFFSET; paintContext.getGraphics().drawString(dayOfMonthString, (float) getX() + CalendarNode.TEXT_X_OFFSET, y); if (hasWidthFocus && hasHeightFocus) { paintContext.pushClip(getBoundsReference()); for (int i = 0; i < lines.size(); i++) { y += 10; g2.drawString((String)lines.get(i), (float) getX() + CalendarNode.TEXT_X_OFFSET, y); } paintContext.popClip(getBoundsReference()); } } }
class DayNode : PNode { bool hasWidthFocus; bool hasHeightFocus; ArrayList lines; int week; int day; String dayOfMonthString; public DayNode(int week, int day) { lines = new ArrayList(); lines.Add("7:00 AM Walk the dog."); lines.Add("9:30 AM Meet John for Breakfast."); lines.Add("12:00 PM Lunch with Peter."); lines.Add("3:00 PM Research Demo."); lines.Add("6:00 PM Pickup Sarah from gymnastics."); lines.Add("7:00 PM Pickup Tommy from karate."); this.week = week; this.day = day; this.dayOfMonthString = ((week * 7) + day) + ""; Brush = Brushes.Black; } public int Week { get { return week; } } public int Day { get { return day; } } public bool HasHeightFocus { get { return hasHeightFocus; } set { hasHeightFocus = value; } } public bool HasWidthFocus { get { return hasWidthFocus; } set { hasWidthFocus = value; } } protected override void Paint(PPaintContext paintContext) { Graphics g = paintContext.Graphics; g.DrawRectangle(Pens.Black, Bounds.X, Bounds.Y, Bounds.Width, Bounds.Height); float y = (float) Y + CalendarNode.TEXT_Y_OFFSET; g.DrawString(dayOfMonthString, CalendarNode.DEFAULT_FONT, Brush, (float) X + CalendarNode.TEXT_X_OFFSET, y); if (hasWidthFocus && hasHeightFocus) { paintContext.PushClip(new Region(Bounds)); for (int i = 0; i < lines.Count; i++) { y += 10; g.DrawString((String)lines[i], CalendarNode.DEFAULT_FONT, Brush, X + CalendarNode.TEXT_X_OFFSET, y); } paintContext.PopClip(); } } }
This node stores two boolean values that indicate whether it should be expanded vertically and horizontally, a list of appointments, two integer values for the week and the day that the cell lies on, and a string representation of the day of the month.
The paint method draws the day of the month in the upper left corner. And, when the cell has the focus, it also renders the list of appointments, since there will be more space for this information. A cell has the focus when it is expanded both vertically and horizontally. See the picture above.
2. Create a Calendar Node
We will create a node that will be responsible for laying out all of the day nodes within the tabular calendar.
-
Here, we will create the calendar node. Add the following class to your project.
static class CalendarNode extends PNode { static int DEFAULT_NUM_DAYS = 7; static int DEFAULT_NUM_WEEKS = 12; static int TEXT_X_OFFSET = 1; static int TEXT_Y_OFFSET = 10; static int DEFAULT_ANIMATION_MILLIS = 250; static float FOCUS_SIZE_PERCENT = 0.65f; static Font DEFAULT_FONT = new Font("Arial", Font.PLAIN, 10); int numDays = DEFAULT_NUM_DAYS; int numWeeks = DEFAULT_NUM_WEEKS; int daysExpanded = 0; int weeksExpanded = 0; public CalendarNode() { for (int week = 0; week < numWeeks; week++) { for (int day = 0; day < numDays; day++) { addChild(new DayNode(week, day)); } } } public DayNode getDay(int week, int day) { return (DayNode) getChild((week * numDays) + day); } protected void layoutChildren(boolean animate) { double focusWidth = 0; double focusHeight = 0; if (daysExpanded != 0 && weeksExpanded != 0) { focusWidth = (getWidth() * FOCUS_SIZE_PERCENT) / daysExpanded; focusHeight = (getHeight() * FOCUS_SIZE_PERCENT) / weeksExpanded; } double collapsedWidth = (getWidth() - (focusWidth * daysExpanded)) / (numDays - daysExpanded); double collapsedHeight = (getHeight() - (focusHeight * weeksExpanded)) / (numWeeks - weeksExpanded); double xOffset = 0; double yOffset = 0; double rowHeight = 0; DayNode each = null; for (int week = 0; week < numWeeks; week++) { for (int day = 0; day < numDays; day++) { each = getDay(week, day); double width = collapsedWidth; double height = collapsedHeight; if (each.hasWidthFocus()) width = focusWidth; if (each.hasHeightFocus()) height = focusHeight; if (animate) { each.animateToBounds(xOffset, yOffset, width, height, DEFAULT_ANIMATION_MILLIS).setStepRate(0); } else { each.setBounds(xOffset, yOffset, width, height); } xOffset += width; rowHeight = height; } xOffset = 0; yOffset += rowHeight; } } }
class CalendarNode : PNode { public static int DEFAULT_NUM_DAYS = 7; public static int DEFAULT_NUM_WEEKS = 12; public static int TEXT_X_OFFSET = 1; public static int TEXT_Y_OFFSET = 1; public static int DEFAULT_ANIMATION_MILLIS = 250; public static float FOCUS_SIZE_PERCENT = 0.65f; public static Font DEFAULT_FONT = new Font("Arial", 7); int numDays = DEFAULT_NUM_DAYS; int numWeeks = DEFAULT_NUM_WEEKS; int daysExpanded = 0; int weeksExpanded = 0; public CalendarNode() { for (int week = 0; week < numWeeks; week++) { for (int day = 0; day < numDays; day++) { AddChild(new DayNode(week, day)); } } } public DayNode GetDay(int week, int day) { return (DayNode) GetChild((week * numDays) + day); } public void LayoutChildren(bool animate) { float focusWidth = 0; float focusHeight = 0; if (daysExpanded != 0 && weeksExpanded != 0) { focusWidth = (Width * FOCUS_SIZE_PERCENT) / daysExpanded; focusHeight = (Height * FOCUS_SIZE_PERCENT) / weeksExpanded; } float collapsedWidth = (Width - (focusWidth * daysExpanded)) / (numDays - daysExpanded); float collapsedHeight = (Height - (focusHeight * weeksExpanded)) / (numWeeks - weeksExpanded); float xOffset = 0; float yOffset = 0; float rowHeight = 0; DayNode each = null; for (int week = 0; week < numWeeks; week++) { for (int day = 0; day < numDays; day++) { each = GetDay(week, day); float width = collapsedWidth; float height = collapsedHeight; if (each.HasWidthFocus) width = focusWidth; if (each.HasHeightFocus) height = focusHeight; if (animate) { each.AnimateToBounds(xOffset, yOffset, width, height, DEFAULT_ANIMATION_MILLIS).StepInterval = 0; } else { each.SetBounds(xOffset, yOffset, width, height); } xOffset += width; rowHeight = height; } xOffset = 0; yOffset += rowHeight; } } }
The constructor adds all of the day nodes as children to the calendar node. The actual number of days in the calendar is determined by the
numDays
andnumWeeks
fields. TheGetDay
method retrieves a particular day node.The most interesting part of the class is the
LayoutChildren
method. This is where all of the day nodes are sized and arranged in a tabular fashion. Days that are expanded vertically receive a certain percentage of the overall height and days expanded horizontally receive a certain percentage of the overall width. The remaining space is divided equally among the unexpanded days.This method should not be confused with
PNode
'sLayoutChildren
method, which gets called whenever a node's bounds or one of its desendent's bounds change. We could have overridden that method to do our layout. But, since we want the option to animate the layout we create a custom method, which takes ananimate
flag to indicate if the children should be animated to their new positions. When the user clicks on a node, we will call this method with animate set to true. And when the calendar node is resized, we will call this method with animate set to false. -
Next we will add the interaction. Add this code to your the
CalendarNode
defined above. For the Java version, you should add the anonymous event listener class to the constructor.CalendarNode.this.addInputEventListener(new PBasicInputEventHandler() { public void mouseReleased(PInputEvent event) { DayNode pickedDay = (DayNode) event.getPickedNode(); if (pickedDay.hasWidthFocus && pickedDay.hasHeightFocus) { setFocusDay(null, true); } else { setFocusDay(pickedDay, true); } } }); public void setFocusDay(DayNode focusDay, boolean animate) { for (int i = 0; i < getChildrenCount(); i++) { DayNode each = (DayNode) getChild(i); each.hasWidthFocus = false; each.hasHeightFocus = false; } if (focusDay == null) { daysExpanded = 0; weeksExpanded = 0; } else { focusDay.hasWidthFocus = true; daysExpanded = 1; weeksExpanded = 1; for (int i = 0; i < numDays; i++) { getDay(focusDay.week, i).hasHeightFocus = true; } for (int i = 0; i < numWeeks; i++) { getDay(i, focusDay.day).hasWidthFocus = true; } } layoutChildren(animate); }
public override void OnMouseUp(PInputEventArgs e) { DayNode pickedDay = (DayNode) e.PickedNode; if (pickedDay.HasWidthFocus && pickedDay.HasHeightFocus) { SetFocusDay(null, true); } else { SetFocusDay(pickedDay, true); } } public void SetFocusDay(DayNode focusDay, bool animate) { for (int i = 0; i < ChildrenCount; i++) { DayNode each = (DayNode) GetChild(i); each.HasWidthFocus = false; each.HasHeightFocus = false; } if (focusDay == null) { daysExpanded = 0; weeksExpanded = 0; } else { focusDay.HasWidthFocus = true; daysExpanded = 1; weeksExpanded = 1; for (int i = 0; i < numDays; i++) { GetDay(focusDay.Week, i).HasHeightFocus = true; } for (int i = 0; i < numWeeks; i++) { GetDay(i, focusDay.Day).HasWidthFocus = true; } } LayoutChildren(animate); }
When the mouse is released, and the picked day is unfocused, we will set the focus to that day. If the picked day already has the focus, we will remove the focus from that day. The
SetFocusDay
method will determine whether or not each day in the calendar should be expanded vertically or horizontally and set that's node's focus properties accordingly. ThenLayoutChildren
will be called to animate the nodes to their new layout.
3. Create a Fisheye Canvas
Unlike the previous tutorials, we are not going to extend PForm or PFrame in this example. Instead we will make a reusable component that extends PCanvas and we will add that component to our Java or .NET window.
- We extend PCanvas and add the calendar node to the scene-graph.
public class TabularFisheye extends PCanvas { private CalendarNode calendarNode; public TabularFisheye() { calendarNode = new CalendarNode(); getLayer().addChild(calendarNode); setZoomEventHandler(null); setPanEventHandler(null); addComponentListener(new ComponentAdapter() { public void componentResized(ComponentEvent arg0) { calendarNode.setBounds(getX(), getY(), getWidth() - 1, getHeight() - 1); calendarNode.layoutChildren(false); } }); } }
public class TabularFisheye : PCanvas { private CalendarNode calendar; public TabularFisheye() { calendar = new CalendarNode(); Layer.AddChild(calendar); this.PanEventHandler = null; this.ZoomEventHandler = null; } protected override void OnResize(EventArgs e) { base.OnResize (e); calendar.SetBounds(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width - 1, ClientRectangle.Height - 1); calendar.LayoutChildren(false); } }
We turn off the default pan and zoom handlers. And we listen to resize events to set the bounds of the calendar node and lay out its children.
4. Add the Canvas to a Window
Now we are ready to add our new component to the window.
-
We create a
JFrame
in Java or aForm
in .NET as a wrapper for our component. Add the following class to your project.public class TabularFisheyeTester extends JFrame { public TabularFisheyeTester() { setTitle("Piccolo2D Tabular Fisheye"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); TabularFisheye tabularFisheye = new TabularFisheye(); getContentPane().add(tabularFisheye); pack(); setVisible(true); } public static void main(String args[]) { new TabularFisheyeTester(); } }
public class TabularFisheyeTester : Form { public TabularFisheyeTester() { TabularFisheye tabularFisheye = new TabularFisheye(); Controls.Add(tabularFisheye); tabularFisheye.Bounds = this.ClientRectangle; tabularFisheye.Anchor = tabularFisheye.Anchor | AnchorStyles.Right | AnchorStyles.Bottom; } static void Main() { Application.Run(new TabularFisheyeTester()); } }
First, we create an instance of our TabularFisheye component and add it to the window. In the .NET version, we anchor our new component, so that it will get resized as the window is resized. Java handles this for us.