一篇非常好的关于javaFx布局入门的文章,学习javaFX不可错过的入门文章,翻译自http://weblogs.java.net/blog/aim/archive/2009/01/layout_primer_f.html
不要相信“在RIA中不需要布局管理”这样的鬼话。确实相比于过去20年里传统的矩形用户界面,新兴的界面类提供了更多生动的,易变化和动画功能.它们就其性质而言是“应用程序”,而不只是眩目的广告,必须处理动态内容和用户的交互。
也许一些关于布局管理的苛责是由于我们自己被AWT中的“布局管理器”,一种实现java客户端布局的主要方案给娇惯了。任何曾经设计和构建过一个重要的用户界面的人都知道更多自然的过程是画出东西,或至少在视觉上展示出来。但是即使拥有最杰出的工具,最初的布局仅仅是开始。困难的是如何整合动态的布局行为——如果容器的大小被调整或内容改变了,应该发生什么。Interface Builder, Dreamweaver, and NetBeans/Mattisse这些工具力求能够支持这些获得了不同的成功。事实是,这是一个很艰难的问题,在RIA中图形混合在一起只是增加了复杂性。动态布局现在还得和时间轴一起工作来构建动画和视觉效果,上述工具集从来没有达到过预期。
JavaFx就是以这些需要来设计的。尽管现在还没有FX设计工具来完全揭示更多情况,尝试开发FX的开发人员应该理解最基本的图形布局规则,这也是这编文章下面要讲到的。
Layout Primer for JavaFX1.0
JavaFx提供了一个完全以2D场景图(scene graph)为特征的图形API,用来创建动态的和视觉丰富的接面,然而API并没有提供明确的布局容器类,用来在场景中自动化创建动态布局。未来版本的JavaFX将会更充分地处理自动化布局,但是这篇文章将告诉你如何通过使用1.0的API来最好地实现动态面局(包括使用animated transitions)。
这篇文章要求你有基本的2D场景图(scene graph)概念熟悉JavaFX脚本语言。欲了解更多有关这些先决条件,请参阅:
What Is Dynamic Layout?
动态布局是指动态地摆放、排列在场景图(scene graph)中的元素来响应来自场景内、外的变化,这种变化通常是由用户活动触发的,也包括其它无法预期的情况。例如,用户调整了Stage的大小并期望内容能够调整,填充它或者在移动设备上的用户载入一套缩略图,并期望它们能够水平流动和在屏幕的界限上绕回。此外重要的是能够支持动画(缩小,脉冲,滑动等)而不会干扰其他场景中的节点。
在JavaFx 1.0中,geometry(几何)APIs中的勾子(hook)就是为这个目的而设计的。动态布局可以通过使用(binding)绑定到某个单独节点来响应场景中布局的改变或是通过创建具有一定计算规则来布局子节点的容器类。本文将涉及相关的API的细节。
Understanding Node Bounds Calculations
为了有效地布局场景,其中一点就是必须能够获取到节点的矩形界限。问题是当考虑到界限的许多可变因素时,得到当前节点的界限就变得复杂,例如几何位置(startX/startY, width, radius等),变形(scale,rotate等),效果(shasows,glow等),还有裁剪。事实证明所有的这些变量适用于节点所在的一个特定的序列,并且在变化序列(transformation sequence)中能够查询到节点界限的具体点,这对实现理想布局场景图至关重要。
下图表示了javafx.scene.Node的变化序列,transformation执行是从左至右(effect第一位,translateX/Y最后):
矩形界限是通过javafx.geometry.Rectangle2D类来表现的,这个类提供了minX,minY,maxX,maxY,width,height变量。既然界限可能是在2D坐标系的任何位置,X/Y值通常是没有意义的,在后面的例子会说明这点。
为了查询节点界限在transformation sequence中的某一值,javaFx API在Node类里提供了以下变量:
boundsInLocal (read-only): 节点没有发生坐标变化的界限,包括了不为零的stroke所需的空间,当设置了effect、clip时,界限有可能超出外形的几何形状。
layoutBounds: layoutBounds 被定义成 boundsInLocal加上通过设定transforms[]得到的transforms后,不像其它只读界限变量,layoutBounds有可能赋予或绑定给其它变量,这样做的目的是可以通过确定界限盒(bounding box)来进行布局计算。
boundsInParent (read-only): 所有的改变加上boundInLocal后节点的界限,包括设定了 transforms[], scaleX/scaleY, rotate, translateX/translateY。
通过下面的图更容易理解一些:
一步一步理解节点界限的例子:
例子一:我们看一下这些用具体的代码创建的矩形得到的界限变量。
注意到x和y是javafx.scene.shape.Rectangle的类变量,它们把矩形在自己的坐标空间内放置而不是移动整个节点的坐标空间。我把它做为第一个例子提出是因为它很容易让来自于其它传统工具开发人员范错。例如在Swing中,改变x,y可以有效地移动部件的坐标空间。所有的javafx.scene.shape类中的指定几何形状的变量都是在自己的坐标空间内的( Rectangle 中的 x, y, width, height, Circle 中的 centerX, centerY, radius等),这些位置变量不应该和坐标空间的移动搞混,正如下面所要看到的例子。
例子 2 要矩形和它的坐标系一起移动(面不是让矩形在它的坐标系里面移动),我们使用使用节点的类变量 translateX/translateY来代替。
现在boundsInParent已经发生了改变来反映移动的矩形,而 boundsInLocal 和 layoutBounds依旧保持不变,这是因为它们是相对于矩形的坐标空间的。
例子3: 现在让我通过设定transforms序列中的Scale类型,把相同矩形按比例放大150%。
注意到javafx.scene.transform.Scale默认是以(0,0)点为中心来按比例进行缩放的,所以矩形是从起点向下和向右来增大的。
例子 4: 最后,如果我们使用scaleX/scaleY变量来代替transforms[]序列中的scale,我们会看到不同的结果。
最明显不同的地方就是scaleX/scaleY变量使得节点总是以节点的中心来缩放,而在前一个例子是以原点(0,0)来缩放的。第二个不同是layoutBounds没有把scale计算在内,因为没有设定transforms sequence,这意味着布局这个矩形用的是它没有变形的界限(后面我会解释这是非常有用)。这个区别也适用于旋转(rotation)和移动(translation):如果是通过在 transforms sequence中设定的,layoutBounds 将会把它计算在内,如果通过设定节点的rotate, translateX/translateY 这些变量,layoutBounds 将不会把它计算在内。
Transformation执行顺序:
在开始说布局前,非常重要的一点是关于Transformations的执行顺序。
Transformations是从下到上的(再次说明:effect第一个,translateX/translateY最后)。但是在transforms序列是按照给定的顺序来执行的。例如按照上图的transforms的代码应该像这样:
Rectangle {
width: 50 height: 50
fill: Color.GREEN
transforms: [
Translate { x: 50 }
Scale { x: 1.5 y: 1.5 }
Rotate { angle: 45 }
]}
下图说明了transtorms的效果
如果我们调换transforms中的顺序的话:
Rectangle {
width: 50 height: 50
fill: Color.GREEN
transforms: [
Rotate { angle: 45 }
Scale { x: 1.5 y: 1.5 }
Translate { x: 50 }
]}
我们会看到结果很不一样:
事实上,Node类中存在transforms序列就是为了给transformations和执行的顺序之间足够的弹性。(译注:意思是你可以选择按照transformations规定的顺序,或是使用transforms来自定义顺序)
布局节点
当在场景中布局节点时,通常需要确定它的当前大小来放置到一定的空间内,同时你也必须得知道它当前的位置,这样你才可以通过合适的位移去调整到你想要的位置。这也就是layoutBounds没有包括translateX和translateY的原因。 layoutBounds通常用来查询节点的当前位置和大小,而translateX和translateY是用来设定调整布局的。
例如,如果一个矩形被创建了:
var rect = Rectangle {
x:10 y:10 width:100 height:100
}
后来需要把矩形放到x,y位置:
var x = 20;
var y = 50;
rect.translateX = x - rect.layoutBounds.minX;
rect.translateY = y - rect.layoutBounds.minY;
注意到translateX和translateY是调整节点当前位置(通过layoutBounds.minX, layoutBounds.minY来定义)的增量来得到想要的位置,translateX/translateY不是位置的最终值。
如果布局计算时需要包括节点的transformations时,那么应该使用设定transforms[]变量。比如说当你把一个节点放大,其它节点能够相应的调整来给那个放大的节点更大的空间时。
而与此相反的是,你不想影响布局或其它节点时,你应该通过使用设定scaleX, scaleY, 和rotate变量,这些设定不会改变layoutBounds。当你在场景中实现节点的动画而不想影响其它节点的布局时,这点是非常有用的。javafx.scene.transition中的类已经帮你做好了这方面的工作。
例如,下面的代码展示了通过使用javafx.animation.transition.ScaleTransition给圆加上脉冲效果而不会影响场景中其它的节点。
使用绑定来布局节点
在很多情况下你需要动态地布局某些节点,那么你可以使用javaFx中强大绑定(binding)功能。更多关于绑定的特性,请参阅 current JavaFX Language specification.
例如,如果你想要在一组节点上创建背景,你可以创建一个矩形节点,然后使用使用绑定来确保它的界限和组节点界限一致:
var group = Group {
content: [
Rectangle {
fill:Color.BLUE
translateX: bind group.layoutBounds.minX
translateY: bind group.layoutBounds.minY
width: bind group.layoutBounds.width
height: bind group.layoutBounds.height
}
other nodes...
]
};
通过使用绑定把节点居中放置也是非常有用的。下面的例子是通过创建一个在场景中居中显示的圆,即使stage的大小被调整了:
Stage = Stage {
var scene:Scene;
var circle:Circle; width: 200 height: 200
scene: scene = Scene {
content: circle = Circle {
fill:Color.RED radius:50
translateX: bind (scene.width - circle.layoutBounds.width)/2 - circle.layoutBounds.minX
translateY: bind (scene.height - circle.layoutBounds.height)/2 - circle.layoutBounds.minY
};
};
}
当你有大量的彼此相对的节点按照某种像水平或方格方式布局时,使用绑定方式就会变得烦琐。在这种情况下,能使用能按一定方式布局子节点的容器类就会很方便。
javaFx API中的类javafx.scene.Group,尽管它扮演父节点的角色,便它却不是布局容器。Group类只不过是让节点集中在场景中操作(例如让Group中的节点透明度为50%或是向右移动Group中的节点100像素)。它不是设计用来限制其子节点在某一特定几何区域(它没有可以用来设定长、高的变量),它只是用来呈现所有子节的界限。
例如,如果一个圆(默认圆心是在原点)被放置在Group内,Group将呈现同样的界限并与原点为圆心:
1.0 API提供了两个简单的Group子类,javafx.scene.layout.HBox 和 javafx.scene.layout.VBox. ,它们可以水平或垂直平局子节点。