要求
注意:此文使用的是Flex beta1,当然Flex4 beta2也不会有任何问题
Spark, 作为Flex4 beta的全新组件模型和架构给RIA表格带来了很多惊喜,同时在皮肤、CSS、组件、状态、动画、文本、graphics标签、布局等等方面解决了许多关键性问题.
本文我将会重点关注Spark布局。我会从全局的角度审视Flex3布局和Spark布局的主要不同之处。在简短介绍用法的不同之后我会一步步的创建一个自定义的Spark布局-一个简单的FlowLayout类。
全文所提到的Halo其实就是指代Flex3中的布局、组件、架构。而Spark则指代Flex4 beta中全新的布局、容器以及整体架构。
和Spark架构的其他新特性一样,全新的布局一样是在强大的Halo布局的基础上发展而来的。Flex3开发者将会发现了解通用执行流程以及相应的API以及布局逻辑都是十分容易的 。然而,由于Spark更加注重模块化设计所以在一些地方还是有所不同与改进的。
相同的特性
对于使用MXML的开发者来说,没什么太大改变。像 width
, height
, minWidth
, explicitWidth
, 和percentWidth
些属性以及相应的语法及语义都没有发生变化。仍然支持 left
, right
, top
,horizontalCenter
, 和 baseline
这些样式。
组件开发者同样会发现相似的元素。核心布局管理类以及组件生命周期都没有发生变化。 对于组件开发者来说三个十分熟悉的方法—commitProperties()
, measure()
, 和 updateDisplayList()
—仍然被布局管理器以相同的顺序进行调用,并且失效规则也没有发生变化。
在 measure()
方法调用时组件依旧会设定默认的大小,同样,组件在 updateDisplayList()
方法被调用时依旧会为它的子元素设定大小。
显著的不同
也许最明显的布局不同之处就在于Spark布局从容器中分离出来了!当一个Spark容器调用 measure()
或updateDisplayList()
时,测量和子元素的放置会交由一个Spark布局实例来完成。这种分离的模式带来了许多其他相关的新颖点:
- 布局逻辑被抽象在一个继承自LayoutBase类的单独类中,LayoutBase是Spark布局的最基础的类
- 全新的API被引入到Spark布局以及它测量、设值、摆放的元素中。这正是ILayoutElement接口的典型应用,并且支持2d和3d变换。如果你不创建自己的布局话,可能并不会注意到这个接口,但如果你这么做的话,你就会发现它是多么的高效。
- 布局 虚拟化—创建,销毁以及回收数据容器的项目渲染器完全交给DataGroup容器(Spark为数据容器所开发的基本构造块)来实现。Spark布局支持虚拟化相对比较容易是因为这一繁重的任务DataGroup负责完成了。
- Spark容器的显示列表顺序与子元素顺序相分离。举例来说,第一个子元素可以被渲染呈现在最后一个子元素的上面
提升的新特性
Spark布局的改变致力于使整个系统更加模块化,功能更强大,更具扩展性。以下就是之前提到的Spark所带来的新特性以及改进的列表:
- 可设定的布局—因为布局逻辑与容器相分离,Spark容器可以被设定不同的Spark布局,即使是在运行的时候。这样减少了容器类的数量,同时提升了模块化以及代码的重用性。例如,在Spark中只有一个List类。为了获得Halo中TileList的功能,只需要为它制定 一个TileLayout实例即可。
- 自定义布局—伴随着布局逻辑与容器的分离,新的LayoutBase类以及ILayoutElement 接口允许开发者更快更容易的开发强大的布局,再将它们和Spark容器组合起来。
- 随意2D变换—Spark布局现在支持随意2D变换。这一功能被内置在ILayoutElement接口中,并且被所有的Spark容器子类所支持。这样一来使得开发支持2D变换的自定义布局变得不费吹灰之力。
- 像素平滑滚动—这一功能被DataGroup所支持,这样一来所有的Spark容器都将支持像素平滑滚动。在自定义布局中实现平滑滚动同样是轻而易举的事。
- 3D 支持—LayoutBase和ILayoutElement接口都支持3D。现在网络上已经出现了3D自定义布局的类,例如CoverFlow,Carousel,WheelLayout。
- 深度管理—开发者可以指定任意子元素的深度,无论在MXML中还是在自定义布局中。
- 后期布局变换—开发者可以指定
x
,y
,z
,rotation
,scale
, 等属性而不会对布局产生任何影响。因为现在所有的Spark布局已经内置了2D变换支持,这一新特性十分实用。假如要做一个移上效果,当鼠标移动到水平布局的列表的一个缩略图上的时候,它会放大一点 或者3d旋转,但这并不会将其他的缩略图挤向右侧。 - 相容坐标空间—所有的大小属性如
width
,height
,measuredWidth
, andmeasuredHeight
保持 一致的预变换。这样就消除了冲突如 "measure()
中measuredWidth
是unscaled
, 但updateDisplayList()
却可以".
因为布局和容器在Spark中是分离的,所以在使用Spark布局时需要注意一下几点改变。花点时间在下面的表格上,你可以直观的看到Halo布局容器类以及与其相对应的Spark布局和容器组合
Halo Containers | Corresponding combination of Spark Layout and Container |
---|---|
Canvas | Group with BasicLayout (no advanced constraints) |
HBox | Group with HorizontalLayout (or the HGroup class) |
VBox | Group with VerticalLayout (or the VGroup class) |
Tile | Group with TileLayout |
List | List with VerticalLayout |
TileList | List with TileLayout |
注意Spark为常用的组合提供了方便的类— HGroup 和 VGroup 类就是拥有HorizontalLayout和VerticalLayout布局的Group类。
MXML 语法
通过将 layout
属性设置为一个Spark布局实例的方式来为容器指定布局方式
<s:List id="list"> <s:layout> <s:HorizontalLayout/> </s:layout> ... </s:List>
所有的布局属性都可以通过layout类实例访问到。以下就展示了如何配置间距和垂直布局。
<s:List id="list"> <s:layout> <s:HorizontalLayout gap="0" verticalAlign="justify"/> </s:layout> ... </s:List>
这种MXML语法还有另一个好处。容器的所有与布局相关的属性都被自然地分离到布局类中。这对于遍历指定布局的所有属性提供了方便,并且可以使用代码提示查看什么是被该布局所支持的。
滚动条
Spark 容器 Group 和 DataGroup 都是轻量级的基本的构造块类。即便如此它们支持滚动,它们不会像 Halo 那样自动装配滚动条。Spark 提供底层 APIs 帮助手动为 Group 和 DataGroup 挂接滚动条-clipAndEnableScrolling, horizontalScrollPosition, verticalScrollPosition, contentWidth,contentHeight
。但同样有一个组件简化了这一过程。将 Group 或 DataGroup 嵌入到 Scroller 中就可以了,Scroller 会处理挂接以及在必要时显示滚动条。
<s:Scroller width="200"> <s:Group> <s:layout> <s:HorizontalLayout gap="0" verticalAlign="justify"/> </s:layout> <s:Button label="one"/> <s:Button label="two"/> <s:Button label="three"/> <s:Button label="four"/> <s:Button label="five"/> </s:Group> </s:Scroller>
事实上,这正是其他Spark容器实现滚动的具体过程。它们的皮肤中包含一个 contentGroup
或 dataGroup
,并且被放置在Scroller中了。查看默认的List皮肤 (spark/skins/spark/ListSkin.mxml)就会发现这一点。
...<!--- The Scroller component to add scroll bars to the list. --><s:Scroller left="0" top="0" right="0" bottom="0" id="scroller" minViewportInset="1" focusEnabled="false"> <!--- The container for the data items. --> <s:DataGroup id="dataGroup" itemRenderer="spark.skins.spark.DefaultItemRenderer"> <s:layout> <s:VerticalLayout gap="0" horizontalAlign="contentJustify" /> </s:layout> </s:DataGroup></s:Scroller>...
当然,所有的Spark滚动API,滚动条,布局,Scroller类原生支持平滑滚动。
Spark布局的一个主要目标就是简化创建自定义布局的过程。即便一个新的布局需要创建一个单独的类,你却只需要几行的代码就可以实现对应的功能。在那之后你可以选择逐渐添加更多功能来满足特定的需求。我将会创建一个简单的FlowLayout类,容器中的所有元素都被水平放置,并且在超过容器宽度的时候会自动换行,具体效果见图1和图2(宽度设置不同)
![宽度设置为212像素 宽度设置为212像素](http://www.adobe.com/content/dotcom/cn/devnet/flex/articles/spark_layouts/_jcr_content/articlecontentAdobe/image.adimg.mw.362.jpg/1281077177630.jpg)
![宽度设置为108像素 宽度设置为108像素](http://www.adobe.com/content/dotcom/cn/devnet/flex/articles/spark_layouts/_jcr_content/articlecontentAdobe/image_0.adimg.mw.363.jpg/1281077189345.jpg)
大小及放置元素
Spark布局的基本功能都是来源于LayoutBase并实现了 updateDisplayList()
方法。以下就是一个空布局的基本框架:
public class FlowLayout extendsLayoutBase{ override public function updateDisplayList(containerWidth:Number, containerHeight:Number):void { // TODO: iterate over the elements of the container, // resize and position them. }}
当一个布局被指定给一个容器时,这个布局的target
属性就被更新成了那个容器。遍历目标的所有元素是十分容易的。数据容器如DataGroup和 List,为每一个将被呈现的数据元素使用项目渲染器实例。创建,回收以及销毁项目渲染器由容器来完成,这些容器同样被认为虚拟化了的,因为项目渲染器被创建、重用只作为所有数据元素的一部分。典型地,支持虚拟化的布局使用滚动位置和大小来计算哪个数据元素进入视野,然后只请求需要显示的数据元素。当布局请求一个元素时,数据容器就会创建并返回与数据项目相对应的项目渲染器。针对这个例子来说,FlowLayout为每一个数据元素请求布局虚拟化的元素超出了本文研究的范文。如果容器是被虚拟化的,我会调用getVirtualElementAt()
方法,否则会调用getElementAt()
方法:
var layoutTarget:GroupBase = target;var count:int = layoutTarget.numElements;for (var i:int = 0; i < count; i++){ var element:ILayoutElement = useVirtualLayout ? layoutTarget.getVirtualElementAt(i) : layoutTarget.getElementAt(i);}
使用ILayoutElement APIs重新修改元素位置和大小同样是十分简单的。这个接口提供了一系列有用的方法查询无论是变换前还是变换后的最小、最大以及首选大小以及获得、设置元素边界。同样提供了操作2D/3D变换矩阵的方法。详细内容超出了本文的范围,这里只提供一个和Halo布局容器使用的APIs的对比列表:
Halo APIs on UIComponent (pre-transform only) | Spark APIs through ILayoutElement (pre- or post-transform) |
---|---|
getExplicitOrMeasuredWidth() | getPreferredBoundsWidth() |
setActualSize() | setLayoutBoundsSize() |
get x, get y | getLayoutBoundsX(), getLayoutBoundsY() |
move() | setLayoutBoundsPosition() |
ILayoutElement APIs在实现updatedisplayList()
方法时被使用:
- 使用
setLayoutBoundsSize(width, height)
重设元素大小,小技巧:传递NaN
将会设为首选值. - 使用
getLayoutBoundsWidth()
及getLayoutBoundsHeight()
获得元素边界. - 使用
setLayoutBoundsPosition(x, y)
定位元素,坐标是相对于容器的。
以下就是updateDisplayList()
的代码(如下代码在案例文件FlowLayout1.as中):
for (var i:int = 0; i < count; i++){ // get the current element, we're going to work with the // ILayoutElement interface var element:ILayoutElement = useVirtualLayout ? layoutTarget.getVirtualElementAt(i) : layoutTarget.getElementAt(i); element.setLayoutBoundsSize(NaN, NaN); // Find out the element's dimensions sizes. // We do this after the element has been already resized // to its preferred size. var elementWidth:Number = element.getLayoutBoundsWidth(); var elementHeight:Number = element.getLayoutBoundsHeight(); // Would the element fit on this line, or should we move // to the next line? if (x + elementWidth > containerWidth) { // Start from the left side x = 0; // Move down by elementHeight, we're assuming all // elements are of equal height y += elementHeight; } // Position the element element.setLayoutBoundsPosition(x, y); // Update the current position, add a gap of 10 x += elementWidth + 10;}
既然FlowLayout功能已经实现了,我们就可以使用类似的MXML语法配置容器了 (FlowLayoutTest.mxml 文件):
<s:List id="list1" width="{widthSlider.value}" height="112" dataProvider="{new ArrayCollection( 'The quick fox jumped over the lazy dog'.split(' '))}"> <!-- Configure the layout to be the FlowLayout --> <s:layout> <my:FlowLayout/> </s:layout></s:List>
滚动支持
FlowLayout运行正常,但是似乎不能滚动。为了实现滚动功能,理论上应当按照如下步骤进行操作 :
- 添加滚动条
- 将滚动条和布局的
horizontalScrollPosition
和verticalScrollPosition
属性挂接起来(这些属性被LayoutBase类所实现). - 计算滚动条的范围,保持同步更新
配置Spark布局的通用方式就是通过相关的属性。对自定义布局添加属性与向Flex类中添加属性没有什么分别。在某些情况下检查target属性是否为空是值得称赞的做法,例如在初始化过程中,一个属性的setter方法可能在target初始化之前被调用。同样需要确保当属性值发生变化时,目标容器大小以及显示列表失效,这样一来LayoutManager就会回调容器重新计算布局。以下就是为FlowLayout设置horizontalGap属性的代码(见FlowLayout2.as 文件)
layoutTarget.setContentSize(maxWidth, maxHeight);
添加属性
配置Spark布局的通用方式就是通过相关的属性。对自定义布局添加属性与向Flex类中添加属性没有什么分别。在某些情况下检查target
属性是否为空是值得称赞的做法,例如在初始化过程中,一个属性的setter方法可能在target
初始化之前被调用。同样需要确保当属性值发生变化时,目标容器大小以及显示列表失效,这样一来LayoutManager就会回调容器重新计算布局。以下就是为FlowLayout设置horizontalGap
属性的代码(见FlowLayout3.as 文件):
public function sethorizontalGap(value:Number):void{ _horizontalGap = value; var layoutTarget:GroupBase = target; if (layoutTarget) layoutTarget.invalidateDisplayList();}
通过LayoutManager被唤醒,容器会将布局交给measure()
和updateDisplayList()
方法。在这个例子中,我将会在为新元素的位置计算中添加horizontalGap
属性。代码放置在updateDisplayList()
循环的底部,如下所示:
// Update the current position, add the gapx += elementWidth + _horizontalGap;
测量容器默认大小
在Flex中,测量决定了一个组件的默认大小。当组件没有明确的大小定义时,默认大小就派上用场了。例如,一个Halo Canvas包含一些文本,但没有明确指定它的width
和height
,这样就会测量它的默认大小以便使它足够大到能显示所有文本。
在Spark中,无论何时一个容器都需要被测量,它的布局的measure()
方法会被调用。通常情况下布局循环遍历所有的元素并且计算理想的区域以便能容纳下所有的元素。布局通过容器的measuredWidth
,measuredHeight
, measuredMinWidth,
和 measuredMinHeight
属性设置默认大小。之后容器可能被重设为它的默认大小,一切都取决于它的某一设置以及它的父容器的设置和布局。最后,布局的updateDisplayList()
方法将会按照容器的最终大小被调用。以下是需要谨记的几点:
-
measure()
方法并不是经常被调用。例如,当容器具有明确的大小时这个方法就会被优化掉 - 支持滚动的容器将会忽略测量大小的最小值。因为用户可以滚动查看内容的任何部分,而不需要考虑容器的大小
对于FlowLayout例子而言,我实现了一个非常简单的测量。measure()
方法计算在一行水平放置的所有元素的默认大小。代码如下(FlowLayout4.as文件):
override public function measure():void{ var totalWidth:Number = 0; var totalHeight:Number = 0; // loop through the elements var layoutTarget:GroupBase = target; var count:int = layoutTarget.numElements; for (var i:int = 0; i < count; i++) { // get the current element, we're going to work with the // ILayoutElement interface var element:ILayoutElement = useVirtualLayout ? layoutTarget.getVirtualElementAt(i) : layoutTarget.getElementAt(i); // In virtualization scenarios, the element returned could // still be null. Look at the typical element instead. if (!element) element = typicalLayoutElement; // Find the preferred sizes var elementWidth:Number = element.getPreferredBoundsWidth(); var elementHeight:Number = element.getPreferredBoundsHeight(); totalWidth += elementWidth; totalHeight = Math.max(totalHeight, elementHeight); } if (count > 0) totalWidth += (count - 1) * _horizontalGap; layoutTarget.measuredWidth = totalWidth; layoutTarget.measuredHeight = totalHeight; // Since we really can't fit the content in space any // smaller than this, set the measured minimum size // to be the same as the measured size. // If the container is clipping and scrolling, it will // ignore these limits and will still be able to // shrink below them. layoutTarget.measuredMinWidth = totalWidth; layoutTarget.measuredMinHeight = totalHeight; }
由于测量时需要考虑horizontalGap
属性,因此改变值时需要使目标容器大小失效:
public function set horizontalGap(value:Number):void{ _horizontalGap = value; // We must invalidate the layout var layoutTarget:GroupBase = target; if (layoutTarget) { layoutTarget.invalidateSize(); layoutTarget.invalidateDisplayList(); }}
测量另一个需要注意的地方--检测像Group一样容器的布局测量是值得称赞的做法,因为它并没有默认的容器大小,使用任何支持皮肤的容器,像List或 Panel,将皮肤的默认大小引入到图片中将会使检测变得异常艰难。案例文件FlowLayoutTestMeasure.mxml表明 FlowLayout测量逻辑在Group容器中完成。
接下去学习什么
作为Flex4 beta的一部分,Spark架构在布局系统上做出了革命性的改变,引入了许多全新精彩的特性。这篇文章只是讲解了Spark布局的一些皮毛知识。
希望获得关于Spark布局规则,语法以及使用案例的话,请访问 在线文档.
希望了解布局APIs更多信息的话请查看 LayoutBase
, ILayoutElement,IViewport
, Scroller
, GroupBase
,DataGroup
以及 Flex 4 beta语言参考中的相关类。同时推荐访问 Hans Muller的博客 因为他对滚动、虚拟化、布局具有深入研究。你同样可以在 我的博客上找到自定义布局相关的话题和案例。
转自:http://www.adobe.com/cn/devnet/flex/articles/spark_layouts.html