WEB开发网
开发学院软件开发Java 创建 Flex 组件 阅读

创建 Flex 组件

 2009-09-28 00:00:00 来源:WEB开发网   
核心提示:富 Internet 应用程序(RIA)正在努力将传统桌面应用程序的交互性、响应性和健壮性引入到基于 Web 的应用程序中,对于希望利用商业智能(BI)和 Web 2.0 技术的开发人员而言,创建 Flex 组件,RIA 尤其重要,Adobe Flex 是使用基于 RIA 解决方案的先行者,您可以找到许多其他博客和 W

富 Internet 应用程序(RIA)正在努力将传统桌面应用程序的交互性、响应性和健壮性引入到基于 Web 的应用程序中。对于希望利用商业智能(BI)和 Web 2.0 技术的开发人员而言,RIA 尤其重要。Adobe Flex 是使用基于 RIA 解决方案的先行者。作为一种相对较新但快速发展的技术,Flex® 利用 Adobe® Flash Player 的功能提供具备高度响应性的出色图像表示。Flex 附带许多有用的、健壮的组件,但当您需要使用 Flex 没有提供的特性并创建特定的功能时,就会面临一些困难。这篇入门级文章深入查看 Flex 的呈现引擎(rendering engine),并介绍将 Flex 组件集成到您的 RIA 中所需的步骤,同时还解释了从头构建新的 Flex 功能所必须了解的知识。

Flex 和 RIA 简介

新的客户机-服务器模型的诞生和对更复杂的 UI 的需求要求基于 Web 的产品采用更丰富和更具响应性的应用程序设计。这些新型的应用程序通常被称为富 Internet 应用程序(RIA),它们的目标是将传统桌面应用程序的许多特征和好处引入到 Web 应用程序中。Adobe Flex 是帮助您创建 RIA 的前沿工具,它包含丰富的图表、动画、3D 效果和响应性更好的用户界面。这些特性对处于萌芽阶段的商业智能(BI)和 Web 2.0 内容的创建和交付尤为重要。核心 Web 2.0 需求通常包括异步处理、请求-响应模式处理和内容聚合等等,Flex 可以帮助您省时省力地处理这些事情。

Flex 附带许多有用的、健壮的组件,但当您需要使用 Flex 没有提供的特性并创建特定的功能时,就会面临一些困难。这篇入门级文章深入查看 Flex 的呈现引擎,并介绍将 Flex 组件集成到您的 RIA 中所需的步骤,同时还解释了从头构建新的 Flex 功能所必须了解的知识。我将特别讲解 Flex 及其功能。我还将减少 Flex 和 Java™ 编程语言之间的相似性,然后从呈现引擎的角度讨论 Flex 组件的生命周期。在这个过程中,我将深入讲解扩展和构建 Flex 组件的细节,包括仔细查看 Flex 组件的生命周期,以及关于 Flex 组件的提交、度量和布局的微妙细节。Flex 组件自动为您处理了许多事情,但是离开这些预构建的组件之后,您很快就会不知所措。我将解析一部分 Flex 组件的工作原理,让您在需要提供这些行为时知道如何做。

Web 开发标准不断变化

Flex 和其他竞争性技术(比如 JavaFX、Ajax 和 Silverlight)都是引领当今 Web 开发变革的先行者,并且对用户希望如何可视化数据产生巨大的影响。Flex 及其竞争对手都是比较新的技术,并且都在尝试克服跨浏览器可移植性、安全性和客户端缓存限制等问题。

Flex 最强大的对手之一是 Ajax。它们的功能有一部分是重叠的,但是它们应用的领域却大不相同。Flex 的特长是丰富复杂的图形结构,而 Ajax 主要用于基于文本的内容。Ajax 通常与 HTML 结合使用,并且用 XML 或 JSON 传输数据,而 Flex 不仅支持 XML 和 JSON 数据传输,甚至还支持二进制格式。在数据传输速率和紧凑性方面,支持二进制数据传输让 Flex 远远领先于 Ajax。

Flex 的优势

Flex 提供了许多引人注目的优势。Flex 的大部分优点源于它构建在广泛使用的 Adobe Flash Player 之上。Flex 的主要优点包括:

完整的浏览器可移植性。任何支持 Flash Player 的浏览器(涵盖了大部分浏览器)都支持 Flex 及其脚本语言 ActionScript (AS)。这与 Ajax 形成鲜明的对比,Ajax 受在不同浏览器中实现的不兼容 JavaScript 的影响。

一致的观感。Flash Player 以向所有操作系统和浏览器提供一致的观感著称。Flex 使用的是 Flash Player 引擎,所以它也将提供一致的观感。

健壮的安全性。Flex 利用经过大量严谨测试的 Flash Player 安全模块。

丰富的 UI。Flex 受益于 Flash Player 的 Halo Skins、渐进填充和矢量图形等特性。

可伸缩的矢量图形(SVG)。Flex 优于大部分基于 RIA 的技术,因为它支持基于矢量的绘制和直接嵌入 SVG 标记文件。基于 SVG 的图像在浏览器支持的分辨率范围内都表现得很好。这与基于位图的图像形成鲜明的对比,因为位图图像在不断放大时会出现明显的失真。

异步请求/响应模型。Flex 为异步处理用户请求提供完整的支持。异步处理支持 Web 站点脱离以页面为中心的模型,这种模型在收到每个用户请求时都会刷新页面。

二进制数据通信。Flex 为 Flex 客户机和后端服务器之间的二进制数据传输提供完整的支持。您可以使用 Adobe 的专用 Action Message Format(AMF)格式或其他自定义格式发送数据。Flex 甚至支持从客户机到服务器的开放二进制套接字,从而实现 “真正的” 数据发送。注意,这个特性不能使用浏览器的加密设施,比如 Secure Socket Layer (SSL)。

运行时共享库(RSL)和模块化。Flex 对这些特性的支持确保您能够动态地加载模块,这意味着您可以向正在运行的应用程序添加新的特性,或利用由运行在相同客户机上的其他 Flex 应用程序加载的 RSL。这能够减少应用程序特性的启动时间,因为它压缩了初始二进制文件的大小。

客户端缓存。Flex 对客户端缓存提供强大的支持。只要用户授权,Flex 应用程序就能在客户端上缓存任意大小的数据,这在接下来的会话频繁请求相同的数据时减少网络数据的往返。您可以缓存任何类型的数据,包括完整的对象图形、定制类、地图和数组。这种支持要比 HTML cookie 先进得多,HTML cookie 仅允许应用程序存储字符串名称值对,并且每个 Web 站点仅能存储 4 KB 名称值对。

跨浏览器通信。Flex 支持在相同类型的浏览器、相同浏览器中的不同选项卡,甚至是相同机器上的不同 浏览器中运行的应用程序之间通信。这个特性意味着许多应用程序能够共享数据,从而实现丰富的终端用户体验。

流线化(Streaming)。Flex 对流线化二进制数据提供出色的支持。对于需要向终端用户传输大量数据的应用程序,流线化是至关重要的,因为它让应用程序向终端用户显示先到达的数据。

强大的后台连接性。在起步时,Flex 就为流行的后台技术提供出色的支持,比如 Java Platform Enterprise Edition、Microsoft .NET 平台、Cold Fusion 和 PHP。连接性支持有助于 Flex 在客户端方面的推广。

丰富的框架。Flex 为组件开发提供一个健壮的框架,并且包含许多为开发人员提供便利的开箱即用组件。这有助于实现快速开发和交付项目。

调试和编辑器支持。Adobe 开发了一个健壮的、基于 Eclipse 的编辑器 Flex Builder,它大大简化了 Flex 应用程序的开发和调试。这是一个非常明智的决策。

这些强大的特性让 Flex 成为创建 UI 的最佳工具之一,不管现在还是将来。使用 Flex 进行开发的另一个好处是:您可以通过组合和重用已经构建好的组件创建一些有趣的解决方案,这非常符合 Web 2.0 的精神。不过,如果您希望从头构建新的定制组件,那么就要困难得多。Flex 提供的强大特性可能会鼓动您去创建复杂完美的组件,让它们实现专属于桌面应用程序(使用 openGL 等库构建)的优势。考虑以 Electronic Arts 赛车游戏 Need for Speed 为模板,在 Web 上编写一个游戏引擎的可能性。或者使用现实中的数据流和链接构建一个 ISP 拓扑。我相信,未来 Flex 将实现这些应用程序以及许多其他类似的应用程序,但是目前的需求和工具都还不太成熟。不管如何,企业的业务依赖于基于 Web 的 UI,并且能够大大受益于 Flex 提供的强大功能。

Flex 与 Java 编程语言的相似之处

Adobe 将 Flex 构建在 Flash Player 之上,而 Flash Player 在呈现丰富 UI 方面是得到认可的技术。Flash Player 非常适合在图形设计器中使用,简化了视频剪辑、时间线、横标广告等的创建。不过,对于已经习惯使用面向对象设计和组件(扩展其他组件)开发应用程序的企业开发人员而言,Flash Player 应用程序并没有那么诱人。另外,Flash Player 的原生语言是 ActionScript,这是一种遵从 ECMA 并且类似于 JavaScript 的脚本语言,而设计者倾向于使用基于 XML 标记的语言。Adobe 及时发现这两者之间的缺口,并通过引入 MXML 将它们连接起来。Adobe 处理 Flex 的方法类似于 Sun™ 处理 Servlets 和 JSP 技术的方法。用 MXML 编写的代码被转换成 ActionScript 代码,后者接着被编译成 Action Byte Code (ABC) —— 这与编译 Java 字节码过程不同。ABC 被用二进制 SWF 格式进行打包,并在用户通过 Web 浏览器请求它时发送。这些基于 SWF 的文件可以在本地运行,也可以在独立的 Flash Player 插件中运行。不过,Flash Security 模块不允许本地文件访问网络资源,反之亦然。

健壮的安全模型

这种安全性预防意味着通过网络运行的应用程序不能访问本地客户机的文件系统 —— 这个功能给 Java Applets 带来了许多恶评。就像 JVM 需要一个 main 方法一样,Flex 应用程序需要一个扩展 mx.core.Application 的组件作为入口点。就像 Java 应用程序运行在 JVM 中一样,SWF 文件最终运行在一个称为 ActionScript Virtual Machine (AVM) 的虚拟机中。表 1 显示了 Java 和 Flex 的虚拟机的相似性对比。

表 1. Java 和 Flex 的虚拟机的对比

FlexJava
MXML 标记和组件JSP 标记和组件
转换为 Action Script Files 转换为 Servlets Classes
编译成 ABC (ActionScript Byte Code) 编译成 JBC (Java Byte Code)
最终得到一个 .swf 文件 最终得到一个 .class 文件
在 ActionScript Virtual Machine 中运行在 Java Virtual Machine 中运行

Flex 中的类体系结构

Flex 包含大量的类,因而无法在本文中全部描述。不过,仔细查看一些类对了解 Flex 对象模型是非常有用的。图 1 显示了核心的类体系结构:

图 1. Flex 的核心类体系结构

显示 Flex 核心类体系结构的流程图:DisplayObject -> InteractveObject -> DisplayObjectContainer -> Sprite -> FlexSprite -> UIComponent

最顶层的类是 DisplayObject,Flex 将它添加到 Flash Player 的 Stage 对象或通用显示列表。InteractiveObject 处理用户交互,包括键盘和鼠标事件。DisplayObjectContainer 允许您添加子组件,并在特定范围内移动它们。Sprite 和 FlexSprite 是不需要时间线的基类,您可以通过扩展它们编写定制类。不过,这个类集合中的主角是 UIComponent 类,它为您提供编写定制组件所需的基础框架。您通常通过扩展这个类来编写定制代码,并且本文的 样例项目 就是从这个类扩展而来的。

Flex 将您创建的所有组件作为最终由 Flash Player 处理的通用显示列表的子组件。Flex 的体系结构隐藏了这些细节,但它提供连接到事件监听器的钩子,让您能够在高级阶段处理事件。Flash Player 的事件分发机制非常健壮,能够处理上万个事件。

构建一个简单的 Flex 组件

到目前为止,我讨论了 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. 完成后的节点元素

粉红色的漩涡背景图由淡蓝色的边框和灰蓝色的边框包围,中间显示文本 ‘John Michael’

构建链接

快要完成了!最后步骤之一是创建一个链接来连接节点。构建链接比构建节点要简单得多。清单 16 显示了如何扩展 UIComponent、覆盖 updateDisplayList() 并使用链接组件的 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

这个图片显示了一个灰蓝色的背景,其上包含 3 个用浅蓝色三角形线条连接起来的方框。位于三角形顶部的方框是背景为粉红色漩涡、中心显示文本 ‘John Michael’ 的方框。位于三角形底部左边的方框是背景为粉红色漩涡、中心显示文本 ‘Paul Simpson’ 的方框。位于三角形底部右边的方框是背景为粉红色漩涡、中心显示文本 ‘Harris Scott’ 的方框。

您可以从 下载 小节获取本文描述的定制 Flex 组件的完整源代码。

至此,您已经创建了一个包含 3 个节点的树图组件。添加更多的节点和链接非常简单,并且与 Flex 呈现引擎无关,因此留给您处理。您可以使用这种类型的组件表示组织树图关系、路由链接拓扑和类似的应用程序。您甚至不需要创建这种类型的应用程序,您可以使用本文讨论的技术和样例构建可扩展的定制 Flex 组件,以满足特定的业务需求。

结束语

本文在某些方面深入讨论了 Flex 的呈现引擎,包括如何扩展内置 Flex 组件,以及如何从头构建定制 Flex 组件。但是,Flex 还有许多地方值得我们去探索。例如,布局阶段是 Flex 组件的生命周期的重要部分,但还有其他重要阶段,包括初始化阶段和创建阶段。Flex Language Reference是了解 Flex 特性的好地方。如果要构建复杂的 RIA,理解事件分发模型也是非常重要的。最后,您可以找到许多其他博客和 Web 站点,它们展示了令人称奇的 Flex 组件,甚至还常常包含源代码 —— 我在参考资料小节列出了其中一部分。

本文示例源代码或素材下载

Tags:创建 Flex 组件

编辑录入:爽爽 [复制链接] [打 印]
赞助商链接