by Joe Ward

Joe Ward


9 June 2010

Prerequisite knowledge

General experience of building applications
with Flex Builder is suggested. For more
details on getting started with this Quick
Start, refer to Building the Quick Start
sample applications with Flex
Required products

Adobe AIR
Flash Builder (Download trial)
Sample files

User level

Rectangular windows are fine, and appropriate for most applications. However, just because they are the easiest to draw doesn't mean that all your windows have to be curve impoverished. The !Square sample application, shown in Figure 1, creates a window based on the ellipse, rather than the rectangle. The window uses vector graphics for most of its chrome, so there are no issues with bitmap scaling to limit the window size or aspect ratio.
The !Square sample application illustrates how to extend the NativeWindow class to create windows with alternative visuals and behavior using the Adobe AIR APIs and the rich Flash graphics capabilities.
The !Square sample application demonstrates how to create windows with alternative visuals.
Figure 1. The !Square sample application demonstrates how to create windows with alternative visuals.
Note: This is a sample application provided, as is, for instructional purposes.

Testing the application

Download and launch the !Square installer (!Square.air) file. Resize the window using any of the eight grippers along the window drag bar. Move the window using the drag bar. Drag the white disk to the edge of the window to observe that any part of the disk which is outside the window client area is properly clipped.

Understanding the code

The !Square application uses several graphics functions not specific to AIR. For more information about these functions, see the ActionScript 3 Reference for the Adobe Flash Platform.
Extending the NativeWindow class
The RoundWindow class extends the AIR NativeWindow class to specialize the constructor and to define the methods and properties to draw its window chrome. Because you cannot use a custom class for the initial application window, the !Square example uses the initial window primarily to launch an instance of the RoundWindow class to serve as the main application window. The initial window created by AIR is never made visible and is closed once initialization is complete.
The constructor of the RoundWindow class creates its own NativeWindowInitOptions object and uses it to create the underlying native window. The constructor then creates the window chrome elements and activates the window to make it visible and active.
public function RoundWindow(title:String=""){ var initOptions:NativeWindowInitOptions = new NativeWindowInitOptions(); initOptions.systemChrome = NativeWindowSystemChrome.NONE; initOptions.transparent = true; super(initOptions); this.minSize = new Point(350,350); bounds = new Rectangle(0,0,viewWidth,viewHeight); this.title = title; stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; stage.addChild(clippedStage); //Put buttons on conventional side of window if(isWindows()){ closeButton = new CloseButton(287); minRestoreButton = new MinRestoreButton(283.5); maxButton = new MaxButton(280); } else { closeButton = new CloseButton(252); minRestoreButton = new MinRestoreButton(255.5); maxButton = new MaxButton(259); } addWindowDressing(); addEventListener(NativeWindowBoundsEvent.RESIZE,onBoundsChange); draw(viewWidth,viewHeight); activate(); }
Drawing elliptical borders
The border of the window is drawn using standard Flash drawing functions provided by the Graphics class, which can be accessed through the graphics property of any display object. The border is composed of three rings. When you draw overlapping figures between the calls to beginFill() and endFill(), only the difference between the figures, in this case ellipses, are filled. The following function draws the three rings by drawing two ellipses for each ring:
with({ clear(); beginFill(bevelColor,1); drawEllipse(0,0,viewWidth,viewHeight); drawEllipse(4,4,viewWidth-8,viewHeight-8); endFill(); beginFill(borderColor,1); drawEllipse(4,4,viewWidth-8,viewHeight-8); drawEllipse(16,16,viewWidth-32,viewHeight-32); endFill(); beginFill(bevelColor,1); drawEllipse(16,16,viewWidth-32,viewHeight-32); drawEllipse(20,20,viewWidth-40,viewHeight-40); endFill(); }
Drawing a section of an ellipse
Because the Graphics class does not have a function for drawing just a piece of an elliptical arc, drawing the resizing gripper onto the border is more challenging than drawing the border itself. You could use the curveTo() method, but calculating the proper control point locations to match the elliptical border has its own mathematical challenges. For this task, !Square uses a simpler polyline technique based on the parametric equation of the ellipse. A point on the border is calculated based on the height and width and angle from the midpoint of the ellipse. Then the next point is calculated by increasing the angle a small amount and a line is drawn between them. This process is repeated until the desired arc is drawn.
Figure 2 shows the formula that can be used to calculate a point along the ellipse, given the angle, and the width and the height of the ellipse.
Flex Text Editor
Figure 2. Use this formula to calculate a point along the ellipsis.
The grippers are arranged around the border at preset angles, so the angle is known. The width and height of the ellipse are the width and height of the window for the outer edge of the window border (viewWidth and viewHeight), and the width and height of the window minus the thickness of the border for the inner edge of the window border.
The drawing routine uses the following formula to calculate the x and y coordinates of the four corners of the gripper, points A, B, C, and D:
var A:Point = new Point(); A.x = Math.cos(startAngle) * viewWidth/2; A.y = Math.sin(startAngle) * viewHeight/2; var B:Point = new Point(); B.x = Math.cos(startAngle) * (viewWidth-40)/2; B.y = Math.sin(startAngle) * (viewHeight-40)/2; var C:Point = new Point(); C.x = Math.cos(stopAngle) * (viewWidth-40)/2; C.y = Math.sin(stopAngle) * (viewHeight-40)/2; var D:Point = new Point(); D.x = Math.cos(stopAngle) * viewWidth/2; D.y = Math.sin(stopAngle) * viewHeight/2;
The start and stop angles are calculated by adding and subtracting half the angular width of the gripper from the preset gripper angle. Defining the length with angles rather than a distance makes the math a bit easier and also provides a more pleasing effect when the window is resized since the gripper length stays proportional to the border diameter.
var startAngle:Number = (angleRadians - spreadRadians); var stopAngle:Number = (angleRadians + spreadRadians);
The routine next draws the shape of the gripper between the four points (see Figure 3).
The shape of the gripper
Figure 3.The shape of the gripper as drawn by the routine described below.
First, a straight line is drawn from A to B:
moveTo(A.x,A.y); lineTo(B.x,B.y);
Next, a series of line segments is drawn between B and C. If enough segments are used, then the visual effect is indistinguishable from an actual curve. !Square uses ten segments, which seems sufficient.
for(var i:int = 1; i < 10; i++){ lineTo(Math.cos(startAngle + i * incAngle) * (viewWidth-40)/2, Math.sin(startAngle + i * incAngle) * (viewHeight-40)/2); }
Another straight line is drawn from C to D and the shape is closed by drawing a polyline segment from D back to A. The shape is started with the beginBitmapFill() method so when endFill() is called the area defined by the drawing commands is filled with a bitmap texture.
Clipping the client area
The RoundWindow class uses a sprite with a clipping mask applied as the container for its contents. Any content objects that are drawn outside the border of the window are clipped. Window chrome elements are added directly to the stage so that they are not clipped.
Clipping can be implemented in Flash by setting the mask property of a Sprite object with another Sprite object. The masking sprite is not drawn, but only the parts in the first sprite that fall under the shape defined by the mask's graphics commands are visible.
In !Square, the clipped sprite is defined by the ClippedStage class. This class creates a clipping mask using the familiar commands for drawing an ellipse based on the width and height of the window. Any part of an object added as a child of the ClippedStage object that falls outside the ellipse are clipped.
private function setClipMask(bounds:Rectangle):void{;,1);,0,bounds.width,bounds.height);; }
The class also listens for resize events from the parent window and responds by redrawing the clipping mask based on the new window dimensions.
private function onResize(event:NativeWindowBoundsEvent):void{ setClipMask(event.afterBounds); }
This type of clipping is not limited to simple shapes, so the technique can be used for any window that has areas which should be masked.
Resizing an elliptical window
To resize the window, the border and grippers must be redrawn based on the new width and height of the window. The resize event object includes an afterBounds property that reports the new dimensions. The width and height from this property are passed to the window draw() method.
private function onBoundsChange(boundsEvent:NativeWindowBoundsEvent):void{ draw(boundsEvent.afterBounds.width, boundsEvent.afterBounds.height); }
Handling content in an elliptical window
Because the content in the !Square window is aware of its container (the springs are attached to the border), it must react to changes in window size and shape. It does this by listening for the window resize event and recalculating the dependent variables.
private function onResize(event:NativeWindowBoundsEvent):void{ center.x = event.afterBounds.width/2; center.y = event.afterBounds.height/2; eye.x -= event.afterBounds.x - event.beforeBounds.x; eye.y -= event.afterBounds.y - event.beforeBounds.y; for each (var spring:Spring in springs){ spring.outerEnd = calculateSpringAnchorPoint(spring.anchorAngle); } }
Since the content is always animated, it does not need to be redrawn by the resize listener. Redrawing occurs on the next frame event anyway. For non-animated content, you may need to resize or reposition the window content objects directly.