
Adobe
对于大多数应用程序, 使用矩形窗口都很合适。但是, 并非由于绘制此类窗口最容易, 就意味着所有窗口都不能使用曲线。图 1 中显示的 !Square 范例应用程序以椭圆而非矩形为基础创建窗口。此窗口对其大部分镶边都采用矢量图形, 因此没有位图缩放的问题, 从而不会限制窗口的大小或高宽比。
!Square 范例应用程序演示如何扩展 NativeWindow 类, 从而使用 Adobe AIR API 和丰富的 Flash 图形功能创建具有新颖视觉效果和行为的窗口。

图 1。!Square 范例应用程序演示如何创建具有新颖视觉效果的窗口。
注意: 本范例应用程序按原样提供, 用于教学目的。
若要充分利用本篇文章, 您需要以下软件和文件:
本范例应用程序包括以下文件和类:
应具备使用 Flex Builder 构建应用程序的一般经验。有关使用此快速入门指南的详细信息, 请参阅用 Flex 构建快速入门范例应用程序。
下载并启动 !Square 安装程序文件 (!Square.air)。使用窗口拖动条上八个手柄中的任意一个, 调整窗口的大小。使用拖动条移动窗口。将白色圆盘拖至窗口边缘将观察到圆盘位于窗口工作区之外的任何部分都受到正确的剪裁。
!Square 应用程序使用了若干非 AIR 所特有的图形函数。有关这些函数的详细信息, 请参阅《ActionScript 3.0 语言参考》*。
RoundWindow 类扩展 AIR NativeWindow 类, 用于使构造函数专门化以及定义用于绘制其窗口镶边的方法和属性。由于对应用程序的初始窗口无法使用自定义类, 因此 !Square 范例使用初始窗口主要是为了启动 RoundWindow 类的实例, 以其作为应用程序的主窗口。AIR 所创建的初始窗口从不可见, 并在初始化完成后即关闭。
RoundWindow 类的构造函数创建其自身的 NativeWindowInitOptions 对象, 并使用该对象创建基础的原有窗口。随后构造函数创建窗口镶边元素, 并激活该窗口, 使其可见且活动。
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);
//将按钮按常规方式放在窗口的一侧
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();
}
使用 Graphics 类 (可以通过任何显示对象的 graphics 属性访问) 所提供的标准 Flash 绘图函数绘制窗口的边框。边框由三个圆环组成。在调用 beginFill() 和 endFill() 之间绘制重叠的图形时, 将仅填充几个图形 (在本例中为几个椭圆) 之间的不同之处。以下函数绘制三个圆环, 其中对每个圆环都绘制两个椭圆:
with(border.graphics){
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();
}
由于 Graphics 类没有仅绘制一段椭圆弧的函数, 因此在边框上绘制调整大小手柄比绘制边框本身更加困难。可以使用 curveTo() 方法, 但计算适当的控制点位置以匹配椭圆边框时, 在数学计算方面有一些难度。为此, !Square 使用一种更简单的折线技术, 该技术以椭圆的参数方程为基础。根据与椭圆中点之间形成的高度、宽度和角度, 计算边框上的一个点。然后通过少量增大角度来计算下一个点, 并在两点之间绘制一条线段。重复此过程, 直到绘制完所需的弧为止。
图 2 显示给定角度以及椭圆的宽度和高度, 即可用于计算椭圆圆周上某点的公式。

图 2。使用此公式计算椭圆圆周上的某点。
手柄围绕边框以预设的角度排列, 因此角度是已知的。椭圆的宽度和高度是窗口边框外边缘的窗口宽度和高度 (viewWidth 和 viewHeight), 以及该窗口的宽度和高度减去窗口边框内边缘的边框厚度。
绘图例程使用以下公式计算手柄四角 (即点 A、B、C 和 D) 的 x 和 y 坐标:
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;
通过将预设的手柄角度加减手柄角宽度的一半, 计算起始角和终止角。以角度而非距离来定义长度使数学计算稍微简便一些, 还能在调整窗口大小时产生更满意的效果, 这是因为手柄长度与边框直径成比例。
var startAngle:Number = (angleRadians - spreadRadians); var stopAngle:Number = (angleRadians + spreadRadians);
接下来, 例程在四个点之间绘制出手柄的形状 (见图 3)。

图 3。手柄的形状, 由下述例程绘制。
首先, 从 A 至 B 绘制一条直线段:
moveTo(A.x,A.y); lineTo(B.x,B.y);
接下来, 在 B 和 C 之间绘制一族直线段。如果使用的线段足够多, 则从视觉效果上就与真实的曲线相差无几。!Square 使用了十条线段, 看上去数量很充足。
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);
}
从 C 至 D 绘制另一条直线段, 而从 D 绘制一条折线段回到 A 后此形状即闭合。此形状以 beginBitmapFill() 方法开始, 因此在调用 endFill() 时将以位图纹理填充绘图命令所定义的区域。
RoundWindow 类使用一个应用了剪裁遮罩的子画面作为其内容的容器。在窗口边框之外绘制的任何内容对象都将被剪裁。将窗口镶边元素直接添加至舞台, 以免这些元素受到剪裁。
通过针对另一个 Sprite 对象设置 Sprite 对象的 mask 属性, 可以在 Flash 中实现剪裁。这样将不绘制起遮蔽作用的子画面, 而只有第一个子画面中属于遮罩的图形命令所定义形状范围内的那些部分是可见的。
在 !Square 中, 被剪裁的子画面由 ClippedStage 类定义。这个类使用常见的命令创建剪裁遮罩, 用于根据窗口的宽度和高度绘制椭圆。任何作为 ClippedStage 对象子级添加的对象, 其中超出椭圆的任何部分都将被剪裁。
private function setClipMask(bounds:Rectangle):void{
ellipticalMask.graphics.clear();
ellipticalMask.graphics.beginFill(0xffffff,1);
ellipticalMask.graphics.drawEllipse(0,0,bounds.width,bounds.height);
ellipticalMask.graphics.endFill();
}
这个类还侦听父窗口中的 resize 事件, 并通过根据新窗口尺寸重新绘制剪裁遮罩而作出响应。
private function onResize(event:NativeWindowBoundsEvent):void{
setClipMask(event.afterBounds);
}
此类型的剪裁不仅限于简单形状, 因此可以将该技术用于其中有区域应被遮蔽的任何窗口。
为了调整窗口大小, 必须根据窗口的新宽度和高度重新绘制边框和手柄。resize 事件对象中包括报告新尺寸的 afterBounds 属性。此属性中的宽度和高度将被传递至窗口的 draw() 方法。
private function onBoundsChange(boundsEvent:NativeWindowBoundsEvent):void{
draw(boundsEvent.afterBounds.width, boundsEvent.afterBounds.height);
}
由于 !Square 窗口中的内容能感知到其容器 (子画面与边框相连), 因此该内容必须对窗口在大小和形状方面的变化作出反应。为此, 将侦听窗口的 resize 事件, 并重新计算相关的变量。
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);
}
}
由于内容始终都是动画形式, 因此不需要由 resize 侦听器重新绘制内容。尽管如此, 在下一个帧事件时仍会发生重新绘制。对于非动画形式的内容, 可能需要直接调整窗口内容对象的大小或将其重新定位。
Charles Ward 是一名技术作者, 它喜欢钻研新技术。在之前的 (专业) 职业生涯中, 他曾参与创建开拓性的计算机游戏, 如 Falcon 3.0 和 4.0 以及 Star Trek: A Final Unity。在闲暇时间, Charles 喜欢自由潜水以及和他的孩子们玩耍。