跳转至

04 · 布局

分工

负责方 管什么
Style 本盒 width/height、min/max、padding、margin、overflow
布局容器 子节点如何排列:方向、gap、对齐、flex 增长、滚动

不要试图用 Style 表达「兄弟间距」或「flex grow」——那些在 Row / Column 等方法上。

常用容器

Box

单子节点外壳;可带 Key、点击、Style:

下文示例统一假定已通过 activeTheme := core.ActiveTheme(ctx) 取得当前主题值, 颜色直接引用主题的 Colors 字段(主题是值,不是查询——见 architecture)。

layout.Box(
    text.New("内容"),
).Style(style.New().Padding(16).Background(style.Solid(activeTheme.Colors.Surface)))

可交互:

layout.Box(text.New("点我")).
    Key("tap").
    OnClick(func() { send(Tapped{}) }).
    Style(style.New().Padding(8).Cursor(style.CursorPointer))

Row / Column

layout.Column(
    text.New("标题").Size(20),
    layout.Row(
        button.New("A").OnClick(...),
        button.New("B").OnClick(...),
    ).Gap(8),
).Gap(12)

常用方法:

  • Gap(n) — 子项间距
  • AlignStart / AlignMiddle / AlignEnd 等 — 交叉轴对齐
  • Width / Height / FillWidth / FillHeight / MaxWidth / Padding — 容器尺寸与内边距;Padding 接受 1 个全边值、2 个纵横值、3 个上/横/下值或 4 个上右下左值
  • 配合 Flexible / Expanded 分配剩余空间(参见本章约束说明)

Center

在可用空间中居中子树:

layout.Center(text.New("居中"))

Scroll

内容超出时滚动:

layout.Scroll(layout.Column(/* 很多子项 */).Gap(8))

Scroll 只在内容溢出时预留滚动条槽位。页面高度会变化时(例如设置页切换 分类),使用覆盖式滚动条,避免短页面接在长页面后面时内容宽度跳动:

layout.Scroll(content).Overlay()
// 只隐藏滚动条,不禁用滚轮或触摸滚动,也不预留槽位。
layout.Scroll(content).WithoutScrollbar()

聊天或日志等持续追加内容的列表可用 .FollowEnd():初始停在末尾;用户滚离末尾后 暂停跟随,回到底部后恢复。

滚动视口统一使用 layout.Scroll;原 layout.Scrollbar 已删除。 根外壳和滚动条部件可通过 .Style(...) 调整,滚动条使用 PartTrack / PartThumb。 重复调用 .Style(...) 按顺序合并。

视觉外扩与裁剪

layout.Scroll、layout.List 和 splitpane.New 会自动为 Style 的阴影和 轮廓在视口内部预留空间;Input、Card 等使用公共 Box 外壳的组件无需额外 添加 margin。安全空间只出现在视口边缘,不会被加到每两个列表项之间。 显式的 OverflowHidden 仍是本地裁剪边界;已被该边界裁掉的子内容不会再影响 外层视口。

自绘超出盒子边界的内容可通过 style.VisualOutset 声明最小范围,或在没有 Style 外壳时使用 layout.VisualOutset:

layout.Box(customCanvas).Style(
    style.New().VisualOutset(6, 8, 10, 8),
)

// 直接自绘时:top, right, bottom, left。
layout.VisualOutset(ctx, gtx, customCanvas, 6, 8, 10, 8)

首次发现新的外扩范围时,视口会请求下一帧完成安全边距布局。它不会让这类 绘制越过滚动视口或窗口边界。

Grid / Wrap / Stack 与视觉组件

容器 用途
Grid 网格
Wrap 自动换行流式排列
Stack 叠放
splitpane.New 可拖分割条,来自 widget/splitpane
divider.New 视觉分隔线,来自 widget/divider
Surface / Card 带主题表面/卡片语义的容器

布局组合可参考仓库 examples/showcase,在仓库根目录运行 go run ./examples/showcase。

尺寸怎么设

尺寸写在 Style 上,再交给 Box 或控件的 .Style(...):

layout.Box(
    input.New(m.Name).OnChange(...),
).Style(style.New().Width(320))

// 或
layout.Box(child).Style(
    style.New().Width(200).
        Height(40).
        MinWidth(120).
        MaxWidth(400).
        Padding(12).
        Margin(4),
)

宽高比:

layout.AspectRatio(1, layout.Box(text.New("正方形内容")))

约束直觉

父级给出 最大可用空间;子级在约束内声明自己的尺寸与排列。 Fill / 百分比类能力通过 Style 与容器策略配合(详见规格 03,应用侧记「父约束 → 子声明」即可)。

典型页面骨架

func View(_ *ui.Context, m Model, send ui.Send[Msg]) ui.Widget {
    return layout.Column(
        titlebar.NewPlatform(...),    // 或自定义顶栏
        layout.Row(
            sidebar.New(...),
            layout.Expanded(
                layout.Scroll(layout.Column(
                        // 主内容
                    ).Gap(16).Style(style.New().Padding(24))),
            ),
        ),
        statusbar.New(...),
    )
}

(具体 API 以组件文档与示例为准;骨架思想是「列/行嵌套 + Scroll + Style 内边距」。)

注意

  1. 每帧重建 Widget 树是正常的;不要缓存可变 Widget 状态。
  2. 有状态控件必须有稳定 Key。
  3. 列表项 Key 用业务 id,不要用仅与渲染顺序绑定的临时下标(若顺序会变)。

下一步