Flex 中的类体系结构
Flex 包含大量的类,因而无法在本文中全部描述。不过,仔细查看一些类对了解 Flex 对象模型是非常有用的。图 1 显示了核心的类体系结构:
图 1. Flex 的核心类体系结构
最顶层的类是 DisplayObject
,Flex 将它添加到 Flash Player 的 Stage 对象或通用显示列表。InteractiveObject
处理用户交互,包括键盘和鼠标事件。DisplayObjectContainer
允许您添加子组件,并在特定范围内移动它们。Sprite
和 FlexSprite
是不需要时间线的基类,您可以通过扩展它们编写定制类。不过,这个类集合中的主角是 UIComponent
类,它为您提供编写定制组件所需的基础框架。您通常通过扩展这个类来编写定制代码。
Flex 将您创建的所有组件作为最终由 Flash Player 处理的通用显示列表的子组件。Flex 的体系结构隐藏了这些细节,但它提供连接到事件监听器的钩子,让您能够在高级阶段处理事件。Flash Player 的事件分发机制非常健壮,能够处理上万个事件。
到目前为止,我讨论了 Flex 给 Web 开发带来的主要好处、它的基础体系结构和一些核心类。但是它们对开发人员而言只不过是序幕而已:编写代码和构建解决方案。用 Flex 创建简单的组件是很容易的。假设您需要创建两个文本输入框,当向其中之一输入文本时,将显示用户在两个文本框中输入的文本。您不需要编写任何关于事件处理的代码就可以创建这样的组件。您将创建的简单 UI 类似于图 2。
图 2. 两个交互文本框
清单 1 显示了创建这个简单的应用程序所需的几行代码:
清单 1. 创建一个简单的 Flex UI
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical">
<mx:TextInput id="box1" text="{box2.text}">
<mx:TextInput id="box2" text="{box1.text}">
</mx:Application>
这段代码非常简单干净,但不要仅从表面看:它能完成很多事情。如果您希望查看 Flex 替您完成的所有事情,那么使用编译器标记 keep-generated-actionscript=true
编译这段代码。Application
容器(查看 mx:Application 标记)和 TextInput
控件对您隐藏了许多复杂的代码。只要您的老板同意您扩展、重用和聚合已经存在的 Flex 组件,这就是件好事。不过,当您从头构建自己的组件时,事情就会迅速变得复杂起来。构建定制组件表明 Flex 可以为您自动完成很多事情;您欣赏的许多 Flex 特性并不包含在精简的 Flex 组件中。
创建定制组件的理由
随着 Flex 的成熟,我相信您将看到越来越多的开发人员开始编写自己的定制 Flex 组件,主要原因有两个:
- 大部分 Flex 组件都是迎合大众的需求,现有的控件可能不能满足您的特定需求。修改和扩展现有的组件能够帮助您填补某些空缺,但是如果您要从根本上改变特定组件的行为,那么就必须编写自己的组件。
- 许多新的可视化标准要求您以新的格式表示数据,这些格式没有得到充分支持或不存在。如果您希望在应用程序中利用这些新格式的优势,就必须提供这种功能。
Flex 组件的生命周期
如果您打算构建定制的组件,那么理解 Flex 组件的生命周期是至关重要的。Flex 生命周期的驱动力是 LayoutManager
。看看节选自这个 Flex API 的描述:
“LayoutManager 是 Flex 的度量和布局战略背后的引擎。布局通过 3 个阶段完成:提交、度量和布局。”
第一个阶段是提交,该阶段在 Flex 使用 addChild()
或它在 DisplayObject 类中的变体之一向主显示列表(此后称为 “阶段”)添加组件时发生。注意,Flex 没有向这个列表直接添加组件;在这发生之前需要经过几个中间步骤。首先,Flex 验证组件的所有属性。LayoutManager
调用父对象上的 validateProperties()
,然后遍历调用每个子对象。这称为自上而下排序。如果存在 “脏” 属性,LayoutManager
允许您在验证发生之前提交它们。您可以通过调用 UIComponent
中的保护方法 commitProperties()
进行提交。定制组件必须在验证发生之前覆盖这个方法以提交属性。通过查看 Label
控件能够帮助您更好地了解发生了什么。标签是在应用程序中呈现一行文本的文本字段。您可以在一个标签中更改多个值,包括字体、字体大小、字体颜色和文本等。要在标签中更改与字体相关的东西,您必须提供一个新的字体上下文和一个新的与新字体对应的 UITextField
;这能帮助确保新字体能够正确呈现。
Flex 如何处理提交更改
现在,考虑一下当应用程序更改标签的字体时会发生什么。记住,Flex 的 UI 依赖于异步处理,因此任何更改都不会立即生效。在更改字体时,您可以同时创建一个新的 UITextField
,或者存储一个新的字体上下文和一个表明字体已经更改的标记。当下次呈现该组件时(通常发生在下一次屏幕刷新),您就可以应用新的字体。检查表明字体状态变更的标记是否改变,然后创建一个对应的 UITextField
。有可能在两次屏幕刷新之间多次更改这些属性,但这让变更处理的效率下降。更好的策略是等待下一次屏幕刷新,然后再应用更改。注意,终端用户不会发现任何变化,因为屏幕的刷新频率下降了。这是针对任何异步、事件驱动 UI 的典型用例。应用程序存储需要应用的变更、对这些变更进行排序、然后在适当时间应用它们。您可能要问,什么时候才是最佳时间。这正是 LayoutManager
处理的任务。当您收到一个对组件的 commitProperties()
的回调时,就可以确定这是再次呈现该组件之前的最后一个调用;在这个调用之后 对属性所做的任何变更都不会出现在即将发生的屏幕刷新中。这些变更必须在下一次屏幕刷新和调用 commitProperties()
时才被应用。
这些都好理解,但您可能很想知道如何告知 LayoutManager
根据组件执行验证。假设某人更改了标签的字体,然后您存储了新的字体和相关标记,现在您需要让 LayoutManager
再次调用验证阶段。您可以为相应的组件调用一个称为 invalidateProperties()
的方法来实现验证。现在,我们看看更改标签的文本所需的代码。清单 2 显示了如何处理验证阶段(为了保持简洁,省略了一些代码)。
清单 2. 处理
public function set text(value:String): void {
_text = value;
textChanged = true
invalidateProperties();
}
验证阶段
注意,_text
是临时存储新的文本值的字段,而 textChanged
是用来标记更改的标记。此外还要注意为了通知 LayoutManager
开始验证阶段而进行的 invalidateProperties()
调用。清单 3 显示了 Label
类的 commitProperties()
中的对应代码片段(注意,为了保持简洁我省略了一些代码):
清单 3. 使用 commitProperties
override protected function commitProperties():void{
if (textChanged) {
textField.text = _text;
textChanged = false
}
这个被覆盖的方法演示了当标记被设置为 true 时,如何更新 UITextField
(textField),以及如何将该标记重新设置为 false 以在未来进行标记。如果您希望使用 Flex 创建定制控件,那么这是必须理解的关键点。在清单 2 和清单 3 中讨论的验证阶段展示了 Flex 呈现引擎的 3 大基础支柱之一。这也是最容易被忽视的阶段,有许多高级的组件甚至没有验证阶段。性能考虑是忽略验证阶段的部分原因。在发生更改时就立即应用它们一般不会引起错误,但是无目的、不必要地多次应用变更会导致 Flex 应用程序的性能问题(一般情况是小问题,但也可能是大问题),理解这点非常重要。
开发人员通常在两种情况下编写组件。第一种情况是,编写一个与应用程序用例紧密结合的组件。通常,这种应用程序还不适合重用,因此可以适当牺牲性能以避免组件过于复杂。第二种情况是,编写适合在框架级别使用或由其他开发人员使用的组件。对于这种情况,尽可能提高组件的性能是值得的。一般在第二种情况中才需要认真处理验证阶段。
度量阶段是另一个重要的 Flex 阶段,它比提交阶段更常用。在组件通过了验证之后,LayoutManager
将继续度量组件,使它能够显示在特定上下文中。例如,LayoutManager
可能将组件显示为特定容器的子容器。您需要使用度量阶段,因为它帮助 LayoutManager
确定父容器是否包含滚动条。一些组件可以根据其子组件正常显示所需的空间调整自身的大小。考虑 VBox 示例,它是一个垂直排列其子容器的容器。如果您没有给 VBox 分配 height
或 width
值,它就在每次添加子容器时重新调整大小。它的 height
足以包含所有垂直排列的子容器,除非您设置某些限制。Flex 包含一个称为 measure()
的保护方法,您可以用它度量组件。LayoutManager
在适当的时间(一般在下一次屏幕刷新之前)调用这个方法,您需要在这个时间点度量组件并使用 measuredWidth
和 measuredHeight
设置 height
和 width
。然后,父容器使用这些属性度量它本身。
首先度量子容器
与验证阶段相反,度量阶段是自下而上的。这是因为必须在度量父容器之前度量子容器。这确保当父容器收到度量调用时,已经度量了它的子容器。到目前为止,一切进展顺利。但是,现在假设您显式地指定了组件的 height
和 width
。Flex 将这些存储的值指定为 explicitWidth 或 explicitHeight 属性。Flex 将考虑您在这种情况下指定的 width
和 height
值。不过,有时给容器设置显式度量是不明智的,因为您可能不能预先知道容器的所有子容器的总大小。 对于这种情况,Flex 给超出指定界限的子容器添加一个滚动条。注意,这些滚动条仅出现在扩展 Container
类的容器上。对于其他控件,滚动条可能出现,或者 Flex 会剪切掉一部分内容区域(称为内容切除)以保持您指定的大小。
显式地设置属性值
显式地设置 width
和 height
值将导致一个有趣的问题。如果您编写一个包含其他组件的组件并希望度量容器的大小,那么了解每个子容器的 width
和 height
将有帮助。您可能很想知道怎样才能知道特定组件的大小是被显式地指定的,或通过覆盖度量方法进行度量。回想一下,度量大小包含在 measuredWidth 和 measuredHeight 等属性中,而显式定义的大小包含在 explicitWidth 和 explicitHeight 中。仅有一对度量包含实数值,而另一对度量将包含 Not-a-Numbers (NaN)。Flex 通过提供两个方法解开了这个谜团:getExplicitOrMeasuredWidth()
和 getExplicitOrMeasuredHeight()
。您仅需调用这些方法,而不必担心 height
和 width
值是否被度量或显示地设置。当一个组件被度量之后,就需要调用 setActualSize()
方法,它将设置 width
和 height
属性。只要显式地设置了 height
和 width
,同时也就设置了这些属性。清单 4 演示了使用显式的 width
和 height
值创建文本输入组件有多么简单。清单 4 接收 4 个属性:
清单 4. 输入 width
和 height
<mx:TextInput width="200" height="45"/>
这个例子包含 4 个属性:width
、height
、explicitWidth
和 explicitHeight
。注意,measuredWidth
和 measuredHeight
保持为 NaN。
现在,看一看清单 5,其中没有设置任何显式的 height
或 width
值:
清单 5. 组件度量
<mx:TextInput />
在这个例子中,组件本身提供了度量。就像在 Flex 框架的另一个地方一样,这里的 “默认” width
和 height
值被设置为 measuredWidth
和 measuredHeight
。显式的值仍然保留为 NaN。无论哪种情况,getExplicitOrMeasuredWidth()
和 getExplicitOrMeasuredHeight()
都返回正确的值。类似地,Flex 为 explicitMinWidth
或 measuredMaxWidth
设置最小的大小,以为组件提供最小和最大的界限。当组件的大小超出 maxWidth
指定的界限时,滚动条将显示当前视图区域内看不到的内容。注意,当设置了显式大小时,LayoutManager
将不调用 measure()
。这是有意义的,因为当您显式地定义了大小之后,就没有必要再次度量组件。
调用 invalidateSize() 将告知 LayoutManager
初始化度量阶段。LayoutManager
包含 3 个不同的队列:invalidatePropertiesQueue
、invalidateSizeQueue
和 invalidateDisplayListQueue
。这些队列与在生命周期的某个点上调用 invalidateProperties()
、invalidateSize()
和 invalidateDisplayList()
的组件对应。然后,LayoutManager
从每个队列中处理对应的组件,并调用每个组件的 validateProperties()
、validateSize()
和 validateDisplayList()
方法。然后,这些方法中的默认实现 UIComponent
将调用 commitProperties()
、measure()
和 updateDisplayList()
。您可以覆盖这些方法以编写定制的逻辑。
度量组件可能还涉及到度量文本。然而,文本的行为与其他控件大不相同。度量文本要求您考虑升序、降序和行距等。幸运的是,Flex 提供的一个实用程序简化了这一过程。UIComponent
公开称为 measureText()
和 measureHtmlText()
的方法帮助您度量文本控件。清单 6 显示了如何度量文本字段:
清单 6. 度量文本字段
package components {
import flash.text.TextLineMetrics;
import mx.core.UIComponent;
public class MyLabel extends UIComponent {
private var _text : String;
public function set text(value :
String) : void {
_text = value;
invalidateSize()
}
public function get text() : String {
return _text
}
override protected function measure(): void {
if (!_text) return super.measure();
//measure text here!
var textLineMetrics : TextLineMetrics =
super.measureText(_text);
super.setActualSize(textLineMetrics.width,
textLineMetrics.height);
}
这段代码创建了一个简单的扩展 UIComponent
的 MyLabel
类,因此您仅需关注度量阶段。注意,您将在设置文本时调用 invalidateSize()
,它通知 LayoutManager
在下一次屏幕刷新期间将组件添加到它的 invalidateSizeQueue()
。然后,LayoutManager
调用组件的 validateSize()
方法,它是在 UIComponent
中定义的。最后,validateSize()
调用您曾经覆盖的 measure()
方法。这个调用使您能够设置控件的大小。您可以简单地将 width
设置为 200,并将 height
设置为 45,然后接受最后的结果。MyLabel
类的所有实例将共享相同的大小,这似乎不是好事情,因为它首先就不支持使用类似于 Flex 的健壮 UI 开发工具。但是使用了固定大小时,您就不需要覆盖 measure()
方法,甚至可以在将组件添加到其父组件时定义 width
和 height
。清单 7 显示了度量标签控件的 2 个变体。
清单 7. 度量标签
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="vertical" xmlns:components="article1.components.*">
<components:MyLabel id="label1"/>
<components:MyLabel ="label2"width="200" height="50"/>
</mx:Application>
在这个例子中,将调用 label1
的 measure()
方法,因为您没有显式地定义它的大小。然而,label2
的 measure
没有被调用,因为您显式地定义了控件的大小。注意,Flex 的 Label
类中的 measure()
实现的功能不仅仅是调用 measureText()。您需要处理左边、右边、顶部和底部的边框填充。但基本原理是一样的。
度量阶段是 Flex 组件的生命周期中的重要阶段。尽管这是一个重要的阶段,但您很少需要关注它,除非您从头开始构建组件。大部分定制的 Flex 组件可能忽略了这个阶段,并将它留给 Flex 处理。Flex 在处理类似的琐碎事情方面表现非常出色,让您从乏味的度量任务中解脱出来。不过,如果您需要为 Flex 框架编写一个新的组件,那么就需要精通这个阶段。
布局阶段是 Flex 框架的 3 大生命周期阶段中的最后一个。您需要在这个阶段上花费大量时间,以致于很难将其看作是一个阶段。所有图形工作都发生在这个阶段,包括皮肤和艺术性增强等。
假设您拥有一个经过适当验证和度量的组件。下一步就是考虑它的布局。布局阶段处理组件的许多方面。例如,您需要在这个阶段处理所有背景图像。您还需要打包在这个阶段中需要使用皮肤的所有组件。在这个阶段中,还可以将组件移动到理想位置。再次以 VBox 为例,这是一个垂直排列的容器。当您向它添加子容器时,您必须以垂直的方式排列它们。这种排列发生在布局阶段。
Flash 坐标系的工作原理
我将引导您了解如何使用 Flex 将子容器移动到 VBox 中,但我们首先温习一下 Flash Player 的坐标系。任何组件的左上角都由 x 和 y 值表示。每个组件的 x 和 y 值都与它的直接父组件相关。x 轴在右边为正值,但是 y 轴在下方为正值(这与传统的上方为正值相反)。因此,如果您在 VBox 中放置了一个标签,其 x 和 y 值分别为 30 和 40,标签的左上角距右边 30 像素,并且在 VBox 左上角下方 40 像素处。每个组件都有 mouseX 和 mouseY 属性,它告诉您组件的鼠标指针的相对位置。例如,如果 mouseX 为 -46,mouseY 为 78,那么鼠标指针在组件的坐标系中为向左 46 像素,向下 78 像素。除了局部坐标系之外,Flex 还有一个全局坐标系,该坐标系从整个应用程序的左上角度量 x 和 y 坐标。这些坐标称为 globalX 和 globalY。
Flex 管理大量更改
在其生命周期中,一个组件经历一系列的转换、旋转、缩放、剪切和拉伸,最后才显示在屏幕上。每个组件都有一个与之关联的 matrix
属性;这个属性包含关于所有这些调整的信息。因此,x 和 y 坐标、globalX 和 globalY 坐标都通过这些矩阵转换关联起来。获取 x 和 y 值通常需要向 globalX 和 globalY 应用矩阵转换。从 x 和 y 坐标获取 globalX 和 globalY 坐标值需要执行反向转换。Flex 隐藏了所有这些细节,并提供两个简化这些转换过程的方法:localToGlobal()
和 globalToLocal()
。您可以正确地使用这些方法,而不用担心底层转换。但是,一定要注意:函数是在点级别上工作的,而不是组件级别。让我们再看看一个基于 VBox 的例子。清单 8 在 VBox 内部创建了一个标签:
清单 8. 在 VBox 内部创建标签
<mx:VBox id="box">
<mx:Label id="myLabel" text="I am a label"/>
</mx:VBox>
box 和 myLabel
都有 localToGlobal()
方法。假设您想要获得 label
的全局位置。您的第一个想法可能是使用 label
的父容器的 localToGlobal
,因为 label
的 x 和 y 坐标都与它的父容器 box
的 x 和 y 坐标相对应。不过,myLabel
拥有相同的方法,因此您也将调用该方法。结果是这两种方法都正确,但您需要使用不同的参数调用它们。您可以使用 box.localToGlobal(new Point(myLabel.x,myLabel,y))
或 myLabel.localToGlobal(new Point(0,0))
。当您调用 box.localToGlobal(new Point(myLabel.x,myLabel.y))
时,就是请求 Flex 提供 VBox 中的点的全局位置,其坐标为 myLabel.x
和 myLabel.y
。注意,这与 myLabel
无关。
获得精确的全局坐标
假设 myLabel
的 x 和 y 坐标分别为 40 和 30。在这个例子中,您要求获得 VBox
中的一个点 (40,30) 的精确全局位置。在这里,将对 VBox 矩阵进行转换。myLabel
的坐标系的左上角为 (0,0) —— Flex 组件都是这样。这意味着在第二个选项中(考虑到 myLabel 的坐标系),您要求 Flex 提供在 myLabel
的坐标系中为 (0,0) 的点的全局位置。不同的方法得到相同的结果,因为它们都从不同的坐标系中选择相同的点。如果没有理解这一区别,在本文的后面可能会引起错误和困惑。
添加工具提示
使用 Flex 时,有几个重要的原因要求您获得全局位置。例如,考虑一个典型的用例,即为组件显示工具提示。组件提示被添加到 Flex 应用程序的 systemManager
,因此它们位于全局坐标系的内部。不过,您需要为位于其父组件局部坐标系中的组件显示工具提示。因此,Flex 挑选对应组件的右下角附近的点的局部坐标,然后对其应用 “局部到全局” 的转换。最后,将绘制一个工具提示组件,并将其添加为组件的子工具提示。工具提示被添加到 Flex 应用程序的 systemManager
,然后放置到将被计算的全局点上。现在考虑另一种情况,在某人单击控件之后显示一个菜单,例如浏览器左上角的 File 菜单。您不能将这样的菜单添加为显示 File 选项的标签的子元素。例如,如果您将菜单添加为标签的父元素的子元素,那么 Flex 就将控件垂直放置在该点的下面,因为这是 VBox
的默认行为。如果您将菜单添加为标签的子元素,那么菜单根本就不会显示,因为标签将仅度量它所包含的文本,然后创建一个符合文本的大小。相反,您需要将菜单添加到 systemManager
,它的行为类似于弹出子控件。您可以将添加到 systemManager
的组件放置到整个应用程序区域的任何位置。要将控件准确地放置在标签的下面,您需要将所需的位置转换为全局坐标。例如,在标签的坐标系中标签的底部点为 (0,label.height)
。然后您可以使用标签的 localToGlobal()
转换该点,并将菜单放置到该点上。使用 move()
方法放置菜单,它将任何组件的左上角映射到所选的位置。清单 9 显示了如何将菜单添加到
标签控件。
清单 9. 将菜单添加到标签
<?xml version="1.0" encoding="utf-8"?>
<mx:Label xmlns:mx=http://www.adobe.com/2006/mxml click="showMenu()">
<mx:Script>
<![CDATA[
import mx.controls.Menu;
private function showMenu() : void {
var m : Menu = new Menu();
// populate the menu control with options:
systemManager.addChild(m);
var global : Point = localToGlobal(new
Point(0,height));
m.move(global.x,global.y);
}
]] >
</mx:Script>
</mx:Label>
在这个例子中,单击标签将调用一个事件处理程序,这个程序创建一个新的 Menu 控件并填充它(为了保持简洁,我省略了一些代码),然后将其作为子元素添加到 systemManager
。这个组件将出现在 systemManager
的 rawChildren 列表中。最后,这段代码将菜单控件移动到位于标签控件下面的全局坐标中。注意,转换是从局部坐标系到全局坐标系,从而支持将控件放置到适当位置。
处理容器时,除了局部和全局坐标系之外,还涉及到另一个坐标系。容器默认创建一个内容面板,用于容纳控件的子控件。因此,向容器添加的子控件不是放置在容器的局部坐标系中,而是放置在它的内容面板中,内容面板拥有自己的坐标系。您可能想知道为什么出现内容面板,以及为什么需要内容面板。在容器中,Flex需要能够添加除了容器之外的更多其他东西,比如滚动条、边框和覆盖图等。这些元素被添加到容器中,而内容面板仅负责处理添加的子元素。如果由于滚动条的原因使子元素不能显示在容器的可见区域内,那么它仍然存在内容面板中。您需要使用其他方法将该子元素从局部坐标系转换到内容坐标系。这些方法包括 localToContent()
和 contentToLocal()
。不过,内容坐标系一般用于容器使用子容器的绝对位置的情况。
开始布局阶段
就像提交和度量阶段一样,布局阶段由调用 invalidateDisplayList() 方法开始。这告知 LayoutManager
当前的显示列表已经不再有效,应该进行更新。Flex 添加一个组件来将 LayoutManager
传递给 invalidatedDisplayListQueue
并在下一次 UI 更新时处理这一指令。LayoutManager
在队列的每个组件上调用 validateDisplayList()
,而这个方法将调用一个保护方法 updateDisplayList()
。如果您正在创建定制布局,那么需要在调用 super.updateDisplayList()
之后覆盖该方法,然后替换为希望使用的逻辑。尤其要注意 updateDisplayList()
接受的参数:unscaledWidth
和 unscaledHeight
。这两个值的命名方式为您放置该组件的内容找到了好位置。这两个参数共同定义了组件的界限。现在它们的边界是不限定的。随后,在这个方法的执行完成之后,Flex 将处理缩放。scaleX
和 scaleY
属性允许您更改矢量绘制的缩放比例。默认情况下,scaleX
和 scaleY
的值为 1。每个组件都包含一个受保护的 graphics
对象,Flex 使用它来执行矢量绘制。矢量绘制用于完成各种任务,比如填充背景颜色、边框颜色和渐变等。基于矢量的绘制本身是一个非常宽泛的主题,超出了本文的讨论范围。可以这么说,这个图形对象公开了 drawCircle()
等方法,您可以使用这些方法绘制基本的图像和边框。例如,假设您希望为 Flex Label
控件创建边框和背景颜色以对其进行扩展。在标准的 Label
控件实现中,这些选项都是不可用的,但清
单 10 显示了如何扩展它:
清单 10. 扩展 Flex 的 Label
类
package article1.components {
import mx.controls.Label;
public class MyLabel extends Label {
override protected function updateDisplayList(
unscaledWidth:Number, unscaledHeight:Number): void {
super.updateDisplayList(unscaledWidth,unscaledHeight);
graphics.lineStyle(1);
graphics.beginFill(0xcccccc);
graphics.drawRoundRect (0,0,unscaledWidth,unscaledHeight,5,5);
graphics.endFill();
}
这段代码扩展了 Label
类并覆盖 updateDisplayList() 方法。在该代码中,您绘制了一个圆角矩形,将 width
设置为 unscaledWidth,height
设置为 unscaledHeight,并且将实现圆角的参数 ellipseWidth 和 ellipseHeight 设置为 5,5。最后,这段代码将矩形的背景填充为灰色。您可能注意到该代码没有调用 invalidateDisplayList()
。对于扩展 Flex 的 Label
类的类而言,这是不必要的,因为在 Flex 的许多地方都会发生静默失效。例如,查看清单 11 中的代码
的作用域,这段代码来自前一段代码的 Label
类:
清单 11. 基类中的失效
// some code omitted for clarity
public function set text(value:String): void {
_text = value;
textChanged = true
invalidateProperties();
invalidateSize();
invalidateDisplayList();
}
当标签的文本设置之后,注意 Flex 如何调用这 3 个失效方法。这意味着您不必显式地调用它们。这帮助理解为什么在这里调用全部 3 个失效方法。当文本被设置之后,旧的文本就是 “脏的”,并且必须提交新文本 —— 即提交阶段。新的文本可能大于或小于旧文本,因此必须重新度量界限。有时,该组件还需要更改布局的某些部分。例如,如果旧的标签显示 HTML 格式的文本,而新的文本是简单的纯文本格式,那么就要相应地更改布局。如果文本超出允许的最大大小,那么可能需要删减标签和使用工具提示。所有这些问题都是在 updateDisplayList()
阶段处理的。此外,当调用 addChild()
时,所有 3 个失效方法都会自动调用。Flex API 深入解释了失效方法是如何工作的:
“失效方法很少被调用。一般而言,在组件上设置属性时将自动调用适当的失效方法。”
这里的要点是,大部分用 Flex 编写的组件在其属性被更改时将通知适当的失效方法。这要求您对现有的 Flex 控件进行扩展。不过,如果您从头构建 Flex 控件,那么一定要记得在适当的时间调用失效方法。
序列化
调用这些阶段的次序还向您透露一些信息,即什么时候不能调用它们。考虑这样一个场景,开发人员在扩展 Label 时不小心选择了在 updateDisplayList()
方法中设置文本属性。设置文本属性将调用 validateProperties()
,而后者将调用 commitProperties()
,然后调用 invalidateDisplayList()
,最后调用 validateDisplayList()
和 updateDisplayList()
。最终结果是一个无限递归循环,Flash Player 可能会挂起整个应用程序,甚至可能挂起用户的系统。因此,切忌超越阶段边界。幸运的是,您可以遵循 3 个简单的规则,从而确保您的组件没有跨越边界:
- 除了与大小和位置有关的属性之外,在提交阶段处理所有其他属性,包括设置样式、边框布局、圆角半径、文本、字体和颜色等。一定不要在这个阶段更改可以再次调用 invalidateProperties() 的属性。
- 在度量阶段仅处理与大小相关的属性。一定不要在这个阶段更改可以再次调用 invalidateSize() 的属性。
- 在这个阶段处理与位置、填充、移动、矢量绘制和位图填充等相关的属性。一定不要在这个阶段更改可以再次调用 invalidateDisplayList() 的属性。
特别关注 validateNow()
需要特别提到的另一个方法是 validateNow()
。在创建组件时您可能需要立即度量它以获得它的大小。注意,组件还没有添加到主显示列表,因此 LayoutManager
还不能执行度量。考虑这样一个场景,您需要在画布的最右上角处放置一个图像。您先创建一个图像实例,然后将它移动到右边。然后执行以下操作:Image.move(canvas.width-image.width,canvas.y);
这个代码片段不能工作,因为图像仅被创建,而没有被度量;所以 image.width
为 0。如果您在调用 image.move()
之前调用 image.validateNow()
,Flex 就会强制调用 LayoutManager
,让它经历所有 3 个阶段,包括正确度量图像、为宽度和高度分配有效值的度量阶段。幸运的是,Flex 画布使用了一个基于限制的布局,它允许您为图像指定限制,比如 'right=0'
。这个指令的基本意思是当在 Flex 画布上布置图像时,它将受到这样的限制:它与画布右侧边缘之间的距离为 0。您可以通过为图像指定 right=0
和 top=0
限制实现这种效果。请记住,validateNow()
是一个占用资源很多的方法,除非有充分的理由,否则不要调用它。
从头构建组件
到目前为止,您已经了解使用 Flex 的好处和一些用例,如何操作和扩展 Flex 组件,并且深入了解了 Flex 组件的生命周期。最后一个步骤是将之前讨论的所有呈现特性包含到您从头构建的定制组件中。我将向您介绍所有必需步骤,展示如何构建一个表示组织的结构的树图。注意,这个样例应用程序使用的是虚假数据。
构建节点
第一步是构建组织树的一个节点。图 3 显示了如何通过添加背景图片增强节点的外观。对用于创建图片的图片编辑器没有要求,因此您可以使用自己或您的图形设计师喜欢的图片编辑器。
图 3. 添加背景图片
调用节点类 Node.as
。清单 12 创建了一个节点元素,并使用图 3 作为背景图。
清单 12. 创建节点元素
override protected function updateDisplayList(
unscaledWidth:Number, unscaledHeight:Number): void {
graphics.clear();
var bitmapData : BitmapData = image.content[
"bitmapData"] as BitmapData;
graphics.beginBitmapFill(bitmapData);
graphics.drawRect(0,0,bitmapData.width,bitmapData.height);
graphics.endFill();
graphics.beginFill(0xff0000,.3);
graphics.drawRoundRect(-3,3,bitmapData.width+6,
bitmapData.height+6,10,10);
graphics.drawRoundRect(0,0,bitmapData.width,
bitmapData.height,10,10);
graphics.endFill();
}
graphics.beginBitmapFill()
方法创建了填充节点元素的背景的位图。图 4 显示了 Flex 如何呈现这个组件。
图 4. Flex 呈现元素节点
清单 13 显示了如何为组件
添加滚动效果,当用户滚动到该组件时,它将显示一个 “焦点” 选择:
清单 13. 添加滚动效果
public class Node extendsUIComponent {
private var rollOver : Boolean;
public function Node() {
addEventListener(MouseEvent.ROLL_OVER,
function(e : Event) : void {
rollOver = true;
invalidateDisplayList();
});
addEventListener(MouseEvent.ROLL_OUT,
function(e : Event) : void {
rollOver = false;
invalidateDisplayList();
});
}
override protected function
updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number): void {
if(rollOver) {
graphics.beginFill(0x0000ff,.2);
graphics.drawRoundRect(
-10,-10,bitmapData.width+20,
bitmapData.height+20,10,10);
graphics.drawRoundRect(
-3,-3,bitmapData.width+6,
bitmapData.height+6,10,10);
graphics.endFill();
}
}
}
在这段代码中,您定义了一个布尔变量 rollover 并为 ROLL_OVER
和 ROLL_OUT
添加事件监听器。在前一种情况中,您将 rollover 设置为 true;在后一种情况中,将 rollover 设置为 false。注意,两种情况都会导致显示列表无效,因为您需要 Flex 呈现(或删除)方框周围的焦点矩形。
接下来,您需要为应用程序添加一些文本。在这个步骤中,您在 Node 内部定义了一个标签,并通过覆盖 createChildren() 方法将该标签添加为子元素。您在 Node
类和 commitProperties()
方法中定义了一个 nodeName
属性,然后使用 nodeName
填充标签的文本字段。接下来,覆盖 measure()
方法并使用文本行单位度量文本的 height
和 width
。此外,还度量 Node
组件并将其 measuredWidth
和 measuredHeight
设置为包含在内嵌图片中的 bitmapData
。清单 14 显示给应用程序添加文本所需的步骤:
清单 14. 给 Flex 应用程序添加文本
private var _nodeName : String;
private function set nodeName(
nodeName : String) : void {
_nodeName = nodeName;
invalidateProperties();
}
override protected function createChildren():void {
text = new Text();
addChild(text);
}
override protected function commitProperties(): void {
super.commitProperties();>
text.text = nodeName;
}
override protected function measure(): void {
super.measure();
var metrics : TextLineMetrics = measureText(nodeName);
text.setActualSize(metrics.width+10,metrics.height+5);
measuredHeight = image.height;
measuredWidth = image.width;
}
注意如何在设置 nodeName 时让属性失效。让属性失效是为了告知 LayoutManager
属性发生改变,应该调用提交阶段。您还使用 commitProperties()
方法在提交阶段设置了标签的真
正文本。最后,清单 15 显示了如何向主应用程序文件添加节点元素:
清单 15. 向主文件添加节点元素
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" xmlns:components="article1.components.*">
<components:Node nodeName="John Michael"/>
</mx:Application>
图 5 显示了最终的 Flex 控件的节点元素,包含了焦点矩形。
图 5. 完成后的节点元素
构建链接
快要完成了!最后步骤之一是创建一个链接来连接节点。构建链接比构建节点要简单得多。清单 16 显示了如何扩展 UIComponent
、覆盖 upda
teDisplayList() 并使用链接组件的 graphics
对象创建线条。
清单 16. 创建链接
package article1.components {
import mx.core.UIComponent;
public class Link extends UIComponent {
public var fromNode : Node;
public var toNode : Node;
override protected function updateDisplayList
(unscaledWidth:Number,
unscaledHeight:Number): void {
graphics.clear();
graphics.lineStyle(4,0x0000ff,.5);
graphics.moveTo(fromNode.x+fromNode.width/2,
fromNode.y+fromNode.height/2);
graphics.lineTo(toNode.x+toNode.width/2,
toNode.y+toNode.height/2);
}
}
}
您创建了一个纯粹基于矢量的绘图,并且显示了一
条从源节点到目标节点的连线。清单 17 显示了如何将节点连接在一起,并创建一个树图。
清单 17. 合并所有部分
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:components="article1.components.* ">
<components:Link fromNode="{node1}" toNode="{node2}"/>
<components:Link fromNode="{node2}" toNode="{node3}"/>
<components:Link fromNode="{node1}" toNode="{node3}"/>
<components:Node id="node1" nodeName="John Michael" x="400" y="300"/>
<components:Node id="node2" nodeName="Harris Scott" x="700" y="600"/>
<components:Node id="node3" nodeName="Paul Simpson" x="100" y="600"/>
</mx:Application>
图 6 显示了最终的定制 Flex 组织树图应用程序。
图 6. 最终的 UI
您可以从小节获取本文描述的定制 Flex 组件的完整源代码。
至此,您已经创建了一个包含 3 个节点的树图组件。添加更多的节点和链接非常简单,并且与 Flex 呈现引擎无关,因此留给您处理。您可以使用这种类型的组件表示组织树图关系、路由链接拓扑和类似的应用程序。您甚至不需要创建这种类型的应用程序,您可以使用本文讨论的技术和样例构建可扩展的定制 Flex 组件,以满足特定的业务需求。