Skip to content

布局系统

This content is not available in your language yet.

末语有两种组织界面的方式:

  • xy 手动放置元素;
  • <vbox><hbox> 自动排列元素。

它们可以混合使用。舞台构图、角色立绘和需要重叠的内容通常适合手动定位;菜单、设置项、按钮组等顺序明确的界面更适合自动排列。

普通 <container> 不会替你排列子元素。每个子元素通过自己的 xy 决定位置:

<container x={100} y={80}>
<text text="标题" x={0} y={0} fontSize={48} />
<text text="正文" x={0} y={80} fontSize={28} />
</container>

这种方式适合需要自由摆放或互相重叠的内容。缺点也很直接:某个元素的尺寸变化后,后续元素的位置不会自动调整。

Flex 布局是一种按顺序排列子元素的方法。你先指定排列方向,布局容器再根据每个子元素的尺寸计算位置。

可以把它想成往盒子里依次放东西:

  • 纵向盒子从上到下放置;
  • 横向盒子从左到右放置;
  • gap 控制相邻元素之间的距离;
  • padding 控制内容与盒子边缘的距离;
  • 对齐属性控制整组内容靠前、居中或靠后。

末语目前支持最常用的单行排列能力,使用 <vbox><hbox> 两个独立节点。若你需要更复杂的布局能力,可以向我们反馈。

<vbox> 按照 JSX 中的顺序,从上到下排列直接子元素:

<vbox x={120} y={100} gap={24}>
<text text="开始游戏" fontSize={36} />
<text text="读取存档" fontSize={36} />
<text text="设置" fontSize={36} />
</vbox>

这里不需要为每段文字计算 y。第一项高度变化时,后面的项目会在同一帧重新排列。

<hbox> 从左到右排列直接子元素:

<hbox x={120} y={100} gap={16} alignItems="center">
<sprite src="ui/settings.png" />
<text text="设置" fontSize={36} />
</hbox>

这个例子使用 alignItems="center",让图标和文字在纵向居中对齐。

省略 widthheight 时,布局容器会根据直接子元素计算该方向的尺寸。

VBox 的自动尺寸:

  • 宽度取最宽的子元素;
  • 高度是所有子元素高度、相邻 gap 和上下 padding 之和。

HBox 的自动尺寸:

  • 宽度是所有子元素宽度、相邻 gap 和左右 padding 之和;
  • 高度取最高的子元素。

例如:

<vbox gap={10} padding={20}>
<text text="第一行" fontSize={30} />
<text text="第二行" fontSize={30} />
</vbox>

这个 VBox 会包住两行文字,并在四周留出 20 像素内边距。两行之间有 10 像素间距。

设置 widthheight 后,对应方向使用指定尺寸:

<vbox
width={600}
height={400}
padding={32}
justifyContent="center"
alignItems="center"
>
<text text="居中的内容" fontSize={36} />
</vbox>

显式尺寸不会缩放子元素。内容放不下时会溢出,也不会自动裁剪。需要裁剪时,使用 <clip> 包住布局容器。

<clip width={600} height={400}>
<vbox width={600} gap={20}>
{items}
</vbox>
</clip>

gap 只出现在相邻子元素之间:

<hbox gap={12}>
<sprite src="ui/a.png" />
<sprite src="ui/b.png" />
<sprite src="ui/c.png" />
</hbox>

三个子元素之间有两个 12 像素间隔。容器两端不会因为 gap 增加空白。

padding 设置四边内边距。paddingXpaddingY 可以分别覆盖水平和垂直方向:

<vbox padding={20} paddingX={40} gap={16}>
<text text="水平内边距为 40" fontSize={28} />
<text text="垂直内边距为 20" fontSize={28} />
</vbox>

最终左右内边距是 40,上下内边距是 20

布局中有两个方向:

  • 主轴是子元素的排列方向;VBox 的主轴是纵向,HBox 的主轴是横向;
  • 交叉轴是另一个方向;VBox 的交叉轴是横向,HBox 的交叉轴是纵向。

justifyContent 控制主轴,alignItems 控制交叉轴。

效果
"start" 从起点开始排列,默认值
"center" 整组内容在主轴居中
"end" 整组内容靠主轴末端排列
"space-between" 第一项和最后一项贴近两端,剩余空间平均加入项目间距

这些值在主轴有剩余空间时才会产生明显区别。自动尺寸容器通常刚好包住内容,因此需要设置对应的 widthheight

<hbox width={900} justifyContent="space-between">
<text text="上一页" fontSize={30} />
<text text="下一页" fontSize={30} />
</hbox>

基础 gap 不会被 space-between 替代。若同时设置,两者会相加。

效果
"start" 靠交叉轴起点,默认值
"center" 在交叉轴居中
"end" 靠交叉轴末端

例如,让 VBox 中不同宽度的项目右对齐:

<vbox width={500} alignItems="end" gap={12}>
<text text="短文本" fontSize={28} />
<text text="更长的一段文本" fontSize={28} />
</vbox>

VBox/HBox 决定子元素的基础位置。子元素自己的 xy 会在这个位置上增加视觉偏移:

<vbox gap={20}>
<text text="第一项" fontSize={30} />
<text text="向右偏移" x={40} fontSize={30} />
<text text="第三项" fontSize={30} />
</vbox>

第二项会向右移动 40 像素,但第三项的位置不会跟着改变。也就是说,x/y 可以微调显示位置,不会改变布局为该元素保留的空间。

布局容器的直接子元素不使用 anchor 决定布局位置。pivot、scale、rotation 和 skew 仍然可以改变视觉效果,但不会推动相邻元素。

VBox/HBox 始终按照 JSX 中的原始顺序测量和排列直接子元素。zIndex 只改变同一父节点下直接子节点的绘制与命中顺序,不改变布局位置、自动尺寸、gap 或对齐结果。

<vbox gap={20}>
<hbox zIndex={1}>
<text text="显示模式" fontSize={30} />
<Select />
</hbox>
<hbox>
<text text="背景音量" fontSize={30} />
<Slider />
</hbox>
</vbox>

这里第一行仍然排在第二行之前,但它的整棵子树会在第二行之后绘制。因此,Select 展开时可以覆盖下面的 Slider,重叠区域的鼠标、触摸和滚轮事件也会优先交给第一行中的节点。

zIndex 只在同一父节点下比较。若只给 Select 内部的下拉图片设置 zIndex,它只能改变 Select 内部节点的顺序,不能越过后续表单行。需要覆盖后续行时,应为包含 Select 的 HBox 设置 zIndex;需要覆盖另一列时,还要在两列共同的父节点下调整对应列容器的 zIndex

VBox 和 HBox 可以互相嵌套。常见做法是用 VBox 组织多行,再用 HBox 组织每一行:

<vbox x={120} y={100} gap={20}>
<hbox gap={16} alignItems="center">
<sprite src="ui/volume.png" />
<text text="音量" fontSize={30} />
</hbox>
<hbox gap={16} alignItems="center">
<sprite src="ui/text-speed.png" />
<text text="文字速度" fontSize={30} />
</hbox>
</vbox>

内层布局先根据自己的内容得到尺寸,外层布局再把它当作一个完整项目排列。嵌套层级没有固定限制。

普通 Container 可以作为 VBox/HBox 的直接子元素。它适合把需要重叠或自由定位的内容组合成一个整体:

<vbox gap={24}>
<container>
<sprite src="ui/button.png" />
<text text="开始游戏" x={32} y={18} fontSize={30} />
</container>
<container>
<sprite src="ui/button.png" />
<text text="读取存档" x={32} y={18} fontSize={30} />
</container>
</vbox>

Container 会根据直接子元素的位置、尺寸和 pivot 计算自己的布局尺寸。scale、rotation 和 skew 造成的视觉扩展不会改变这个尺寸。

负坐标内容可以向 Container 左侧或上方溢出,但不会移动 Container 的局部原点。设计可复用组件时,最好让内容从 (0, 0) 附近开始,减少难以察觉的溢出。

图片加载、文本重新排版或子元素增删时,布局尺寸会在同一帧重新计算。需要读取 VBox/HBox 的最终尺寸时,可以使用 onLayout

<vbox
gap={16}
onLayout={(event) => {
console.log(event.width, event.height);
}}
>
{items}
</vbox>

onLayout 只在布局尺寸变化时触发。它适合更新滚动范围、滚动条比例或其他依赖内容尺寸的状态。

visible={false} 只隐藏渲染结果,不会把元素从布局中移除。隐藏的直接子元素仍然占据空间,也仍然计入 gap。若你需要移除某个元素,可以在 JSX 中使用条件渲染。

VBox/HBox 不会自动裁剪溢出内容。需要固定视口时使用 <clip>;需要滚动视图时可以使用 @momoyu-ink/kit 提供的 ScrollView

属性 类型 默认值 说明
width number 自动 显式宽度
height number 自动 显式高度
gap number 0 相邻有效子元素的间距
padding number 0 四边内边距
paddingX number 左右内边距,覆盖 padding
paddingY number 上下内边距,覆盖 padding
justifyContent "start" | "center" | "end" | "space-between" "start" 主轴对齐
alignItems "start" | "center" | "end" "start" 交叉轴对齐
onLayout (event: LayoutEvent) => void 尺寸变化时触发

尺寸、gap 和 padding 应使用有限的非负数。无效值会被当作 0