UiPanels
GWT 中的面板很像其他用户界面库中的布局对应物。主要区别在于 GWT 面板使用 HTML 元素来布局其子小部件。
面板包含小部件和其他面板。它们用于定义浏览器中用户界面的布局。
基本面板
RootPanel
一个 RootPanel 是最顶层的面板,所有其他小部件最终都附加到它。 RootPanel.get() 获取一个包装 HTML 文档的 <body>
元素的单例面板。使用 RootPanel.get(String id) 获取页面上任何其他元素的面板。
FlowPanel
一个 FlowPanel 是最简单的面板。它创建一个单个 <div>
元素,并将子元素直接附加到它,而不进行修改。在您希望自然 HTML 流决定子小部件布局的情况下使用它。
HTMLPanel
此面板提供了一种简单的方法来定义 HTML 结构,小部件将在其中定义的点嵌入。虽然您可以直接使用它,但它最常用于 UiBinder 模板 中。
FormPanel
当您需要重现 HTML 表单的行为时(例如,与期望表单 POST 请求的服务器交互,或者只是在浏览器中获取默认表单键盘行为),您可以使用 FormPanel。此面板包装的任何小部件都将被包装在 <form>
元素中。
ScrollPanel
当您希望在另一个面板内创建可滚动区域时,您应该使用 ScrollPanel。此面板在布局面板(见下文)中效果很好,布局面板为它提供了正确滚动所需的明确大小。
PopupPanel 和 DialogBox
使用这两个面板来创建简单的弹出窗口和模态对话框。它们与浏览器窗口中现有内容重叠,并且可以彼此堆叠。
Grid 和 FlexTable
这两个小部件用于创建传统的 HTML <table>
元素,并且也可以用作面板,因为可以将任意小部件添加到它们的单元格中。
布局面板
GWT 2.0 引入了一系列新面板,这些面板共同构成了快速且可预测的应用程序级布局的稳定基础。有关背景和详细信息,请参阅下面的 “GWT 2.0 布局系统的设计”。
布局系统的大部分都体现在一系列面板小部件中。这些小部件中的每一个都使用底层布局系统以可靠的方式定位其子元素。
RootLayoutPanel
此面板是一个单例,充当根容器,所有其他布局面板都应附加到它(有关详细信息,请参阅下面的 RequiresResize 和 ProvidesResize 部分)。它扩展了 LayoutPanel,因此您可以使用任意约束将任意数量的子元素定位。
您最常将 RootLayoutPanel 用作另一个面板的容器,如以下代码段所示,这将导致 DockLayoutPanel 填充浏览器的客户区
DockLayoutPanel appPanel = new DockLayoutPanel(Unit.EM);
RootLayoutPanel.get().add(appPanel);
LayoutPanel
将 LayoutPanel 视为最通用的布局机制,通常是其他布局构建的基础。它最接近的类似物是 AbsolutePanel,但它更通用,因为它允许其子元素使用任意约束进行定位,如以下示例所示
Widget child0, child1, child2;
LayoutPanel p = new LayoutPanel();
p.add(child0); p.add(child1); p.add(child2);
p.setWidgetLeftWidth(child0, 0, PCT, 50, PCT); // Left panel
p.setWidgetRightWidth(child1, 0, PCT, 50, PCT); // Right panel
p.setWidgetLeftRight(child2, 5, EM, 5, EM); // Center panel
p.setWidgetTopBottom(child2, 5, EM, 5, EM);
DockLayoutPanel
DockLayoutPanel 与现有的 DockPanel 小部件具有相同的目的,只是它使用布局系统来实现此结构,而不使用表格,并且以可预测的方式。您通常会使用它来构建应用程序级结构,如以下示例所示
DockLayoutPanel p = new DockLayoutPanel(Unit.EM);
p.addNorth(new HTML("header"), 2);
p.addSouth(new HTML("footer"), 2);
p.addWest(new HTML("navigation"), 10);
p.add(new HTML(content));
请注意,DockLayoutPanel 要求所有子元素使用一致的单位,这些单位在构造函数中指定。它还要求明确指定每个子小部件的大小(最后一个除外,它将消耗所有剩余的空间),这沿其主轴。
SplitLayoutPanel
SplitLayoutPanel 与 DockLayoutPanel 相同(实际上扩展了它),只是它会自动在每对子小部件之间创建一个用户可拖动的分隔线。它也只支持使用像素单位。使用此方法代替 HorizontalSplitPanel 和 VerticalSplitPanel。
SplitLayoutPanel p = new SplitLayoutPanel();
p.addWest(new HTML("navigation"), 128);
p.addNorth(new HTML("list"), 384);
p.add(new HTML("details"));
StackLayoutPanel
StackLayoutPanel 替换了现有的 StackPanel(它在标准模式下效果不佳)。它一次显示一个子小部件,每个子小部件都与一个“标题”小部件相关联。单击标题小部件将显示其关联的子小部件。
StackLayoutPanel p = new StackLayoutPanel(Unit.EM);
p.add(new HTML("this content"), new HTML("this"), 4);
p.add(new HTML("that content"), new HTML("that"), 4);
p.add(new HTML("the other content"), new HTML("the other"), 4);
请注意,与 DockLayoutPanel 一样,在一个给定面板上只能使用一种单位类型。提供给 add() 方法的长度值指定标题小部件的大小,它必须是固定大小。
TabLayoutPanel
与现有的 TabPanel 一样,TabLayoutPanel 显示一行可点击的选项卡。每个选项卡都与另一个子小部件相关联,当用户单击选项卡时,该子小部件将显示。
TabLayoutPanel p = new TabLayoutPanel(1.5, Unit.EM);
p.add(new HTML("this content"), "this");
p.add(new HTML("that content"), "that");
p.add(new HTML("the other content"), "the other");
提供给 TabLayoutPanel 构造函数的长度值指定选项卡栏的高度,您必须明确提供该高度。
我什么时候不应该使用布局面板?
上面描述的面板最适合用于定义应用程序的外层结构——也就是说,最不像“文档”的部分。对于 HTML/CSS 布局算法效果良好的部分,您应该继续使用基本小部件和 HTML 结构。特别是,考虑使用 UiBinder 模板 来直接使用 HTML,只要这样做有意义。
动画
GWT 2.0 布局系统对动画具有直接的内置支持。这是支持许多用例所必需的,因为布局系统必须正确处理一组布局约束之间的动画。
实现 AnimatedLayout 的面板,例如 LayoutPanel、DockLayoutPanel 和 SplitLayoutPanel,可以将其子小部件从一组约束动画到另一组约束。通常,这是通过设置要动画到的状态来完成的,然后调用 animate()。有关特定示例,请参阅下面的 “食谱”。
RequiresResize 和 ProvidesResize
GWT 2.0 中引入了两个新的特征接口:RequiresResize 和 ProvidesResize。它们用于在小部件层次结构中传播大小调整事件通知。
RequiresResize 提供了一个单一方法,onResize(),该方法在子元素大小发生变化时由小部件的父元素调用。 ProvidesResize 只是一个标记接口,表示父小部件将遵守此约定。这两个接口的目的是在所有实现 RequiresResize 的小部件和 RootLayoutPanel 之间形成一个 unbroken 层次结构,该层次结构监听可能影响层次结构中小部件大小的任何更改(例如浏览器窗口大小调整)。
何时使用 onResize()
大多数小部件不需要知道它们何时被调整大小,因为浏览器的布局引擎应该负责大部分工作。但是,有时小部件确实需要知道。例如,当小部件包含一个动态项目列表,而该列表取决于可用于显示它们的可用空间时,就会出现这种情况。因为让布局引擎完成其工作几乎总是比运行代码更快,所以除非没有其他选择,否则你不应该依赖于onResize()。
何时以及如何实现 ProvidesResize
实现 ProvidesResize 的面板应将调整大小事件传播到其任何实现 RequiresResize 的子小部件。有关此的典型示例,请参见LayoutPanel.onResize() 的实现。大多数自定义小部件将希望使用ResizeComposite 来合成现有的布局面板,但正如下一节所述。
ResizeComposite
在创建包装实现RequiresResize 的小部件的自定义Composite 小部件时,应使用ResizeComposite 作为其基类。此Composite 的子类会自动将调整大小事件传播到其包装的小部件。
迁移到标准模式
GWT 2.0 布局系统旨在仅在“标准模式”下工作。这意味着你应始终将以下声明放在 HTML 页面的顶部:<!DOCTYPE html>
在标准模式下什么不起作用?
如上所述,一些现有的 GWT 面板在标准模式下的行为并不完全符合预期。这主要源于标准模式和怪异模式渲染表格的方式之间的差异。
CellPanel(HorizontalPanel、VerticalPanel、DockPanel)
这些面板都使用表格单元格作为其基本结构单位。虽然它们在标准模式下仍然有效,但它们将以稍微不同的方式布置其子级。主要区别在于它们的子级将不尊重宽度和高度属性(通常将 CellPanels 的子级显式设置为 100% 宽度和高度)。在浏览器将空间分配给各个表格行和列的方式上也存在差异,这会导致标准模式下的意外行为。
你应使用DockLayoutPanel 代替DockPanel。VerticalPanel 通常可以用简单的FlowPanel 替换(因为块级元素将自然地垂直堆叠)。
HorizontalPanel 有点棘手。在某些情况下,你可以简单地用DockLayoutPanel 替换它,但这需要你显式指定其子级的宽度。最常见的替代方法是使用FlowPanel,并在其子级上使用 float: left;
CSS 属性。当然,你也可以继续使用HorizontalPanel 本身,只要你考虑到上述注意事项。
StackPanel
StackPanels 在标准模式下效果不太好。由于上面提到的表格渲染差异,StackPanel 几乎肯定不会在标准模式下按预期执行,你应将它们替换为StackLayoutPanel。
SplitPanel(HorizontalSplitPanel 和 VerticalSplitPanel)
SplitPanels 在标准模式下非常不可预测,你几乎总是应该将它们替换为SplitLayoutPanel。
GWT 2.0 布局系统的设计
在 2.0 之前,GWT 处理应用程序级布局的机制存在一些重大问题
- 它们不可预测。
- 它们通常需要额外的代码来修复其缺陷
- 例如,在没有额外代码的情况下,几乎不可能让应用程序使用内部滚动填充浏览器的客户区。
- 它们在标准模式下并不都能很好地工作。
它们的基本动机是合理的 - 目的是让浏览器的本机布局引擎完成几乎所有工作。但上述缺陷可能是致命的。
目标
- 完全可预测的布局行为。精确的布局应该是可能的。
- 它也应该在存在带有任意单位的 CSS 装饰(边框、边距和填充)的情况下工作。
- 在标准模式下正确工作。
- 让浏览器在其布局引擎中完成几乎所有工作。
- 仅在绝对必要时才进行手动调整。
- 平滑的自动动画。
非目标
- 在怪异模式下工作。
- 基于“首选大小”的 Swing 式布局。这在浏览器中实际上是棘手的。
- 接管所有布局。此设计旨在处理粗粒度的“桌面式”布局。各个片段,如表单元素、按钮、表格和文本应仍然以自然方式布局。
Layout 类
该Layout 类包含布局系统使用的所有底层逻辑,以及用于在各种浏览器上规范化布局行为的所有实现细节。
它实际上与小部件无关,直接在 DOM 元素上操作。大多数应用程序没有理由直接使用此类,但它应该对备用小部件库编写者很有用。
基于约束的布局
GWT 2.0 布局系统建立在 CSS 中存在的简单约束系统之上。这使用属性 left
、top
、width
、height
、right
和 bottom
。虽然大多数开发人员都熟悉这些属性,但鲜为人知的是,它们可以以各种方式组合起来形成一个简单的约束系统。以下 CSS 示例
.parent {
position: relative; /* to establish positioning context */
}
.child {
position: absolute; left:1em; top:1em; right:1em; bottom:1em;
}
在此示例中,子级将自动占用父级的整个空间,减去边缘周围的 1em 空间。这些属性中的任何两个(在每个轴上)构成一个有效的约束对(三个将是退化的),产生了大量有趣的可能性。当考虑各种相对单位的混合(例如“em”和“%”)时,这一点尤其重要。
配方
以下是一系列简单的“配方”,用于创建各种结构和处理不同的场景。在可能的情况下,我们将根据UiBinder 模板来描述布局。
基本应用程序布局
以下示例显示了一个简单的应用程序样式布局,包括标题、左侧边缘的导航区域和可滚动的内容区域。
<g:DockLayoutPanel unit='EM'>
<g:north size='4'>
<g:Label>Header</g:Label>
</g:north>
<g:west size='16'>
<g:Label>Navigation</g:Label>
</g:west>
<g:center>
<g:ScrollPanel>
<g:Label>Content Area</g:Label>
</g:ScrollPanel>
</g:center>
</g:DockLayoutPanel>
你必须将此结构放在实现ProvidesResize 的容器中,最常见的是RootLayoutPanel。以下代码演示了如何执行此操作
interface Binder extends UiBinder<Widget, BasicApp> { }
private static final Binder binder = GWT.create(Binder.class);
public void onModuleLoad() {
RootLayoutPanel.get().add(binder.createAndBindUi());
}
分隔符
SplitLayoutPanel 的工作方式与DockLayoutPanel 非常相似,只是它仅支持像素单位。上面的基本应用程序结构可以像这样在导航和内容区域之间添加一个分隔符
<g:DockLayoutPanel unit='EM'>
<g:north size='4'>
<g:Label>Header</g:Label>
</g:north>
<g:center>
<g:SplitLayoutPanel>
<g:west size='128'>
<g:Label>Navigation</g:Label>
</g:west>
<g:center>
<g:ScrollPanel>
<g:Label>Content Area</g:Label>
</g:ScrollPanel>
</g:center>
</g:SplitLayoutPanel>
</g:center>
</g:DockLayoutPanel>
请注意我们如何混合停靠面板和分割面板,以便可以以 EM
单位指定标题的大小。
布局动画
要将动画与LayoutPanel 一起使用,你必须首先创建一组初始约束,然后动画到目标约束集。在以下示例中,我们从位于顶部但没有高度的子小部件开始,因此它实际上是隐藏的。调用LayoutPanel.forceLayout() 会“修复”初始约束。
panel.setWidgetTopHeight(child, 0, PX, 0, PX);
panel.forceLayout();
现在,我们给小部件一个 2em 的高度,并显式调用 LayoutPanel.animate(int) 以使其在 500 毫秒内调整大小。
panel.setWidgetTopHeight(child, 0, PX, 2, EM);
panel.animate(500);
这将适用于任何约束和任何数量的子级。
实现需要调整大小的复合组件
实现RequiresResize 的小部件期望在小部件大小更改时调用RequiresResize.onResize()。如果你正在将此类小部件包装在Composite 中,你需要使用ResizeComposite 来确保此调用被正确传播,如下所示
class MyWidget extends ResizeComposite {
private LayoutPanel p = new LayoutPanel();
public MyWidget() {
initWidget(p);
}
}
子小部件可见性
该Layout 类必须将其每个子元素都包装在一个“容器”元素中才能正常工作。这意味着,当你调用UIObject.setVisible(boolean) 来设置LayoutPanel 中小部件的可见性时,它不会完全按预期执行:小部件确实会变得不可见,但它往往会消耗鼠标事件(实际上,是容器元素在这样做)。
为了解决此问题,你需要使用LayoutPanel.setWidgetVisible(Widget,boolean)
LayoutPanel panel = ...;
Widget child;
panel.add(child);
panel.setWidgetVisible(child, false);
在没有 RootLayoutPanel 的情况下使用 LayoutPanel
在大多数情况下,你应该通过将布局面板直接附加到RootLayoutPanel 或通过实现ProvidesResize 的其他面板来使用布局面板。
但是,在某些情况下,你需要在普通小部件(例如FlowPanel 或RootPanel)中使用布局面板。在这些情况下,你需要显式设置面板的大小,如以下示例所示
LayoutPanel panel = new LayoutPanel();
RootPanel.get("someId").add(panel);
panel.setSize("20em", "10em");
请注意,RootLayoutPanel 没有提供用于包装任意元素(如RootPanel 所做)的机制。这是因为无法知道浏览器何时调整了任意元素的大小。如果你想在任意元素中调整布局面板的大小,则必须手动执行此操作。
这也适用于在PopupPanel 和DialogBox 中使用的布局面板。以下示例显示了在DialogBox 中使用SplitLayoutPanel 的情况
SplitLayoutPanel split = new SplitLayoutPanel();
split.addWest(new HTML("west"), 128);
split.add(new HTML("center"));
split.setSize("20em", "10em");
DialogBox dialog = new DialogBox();
dialog.setText("caption");
dialog.add(split);
dialog.show();
表格和框架
使用 <table>
或 <frame>
元素实现的小部件不会自动填充布局提供的空间。为了解决此问题,你需要显式将这些小部件的 width
和 height
设置为 100%
。以下示例显示了使用 <iframe>
元素实现的RichTextArea 中的示例。
<g:DockLayoutPanel unit='EM'>
<g:north size='2'>
<g:HTML>Header</g:HTML>
</g:north>
<g:south size='2'>
<g:HTML>Footer</g:HTML>
</g:south>
<g:center>
<g:RichTextArea width='100%' height='100%'/>
</g:center>
</g:DockLayoutPanel>