
Flame 布局系统完全指南用 Row、Column、Expanded、Padding 与 Align 组件声明式排列游戏 UI【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame在游戏开发中用像素坐标手动摆放元素HUD、菜单、血条、技能栏在简单场景下尚可接受但一旦界面需要适配不同屏幕尺寸手算坐标就变得繁琐且脆弱。Flame 引擎将 Flutter 的布局思想行、列、内边距、对齐引入游戏世界通过 layout 模块文档 介绍的AlignComponent、RowComponent、ColumnComponent、ExpandedComponent、PaddingComponent等布局组件你可以用声明式的方式排列组件而不是逐个手工计算位置。读完本文你将掌握 Flame 布局组件体系的全貌、各组件构造参数与行为细节、shrink-wrap收缩包裹与主轴/交叉轴对齐等高级特性并能直接写出可运行的布局代码。布局组件概览把 Flutter 的布局模型带进游戏Flame 的布局组件位于两个位置AlignComponent属于主库的稳定 API实现在 src/layout/align_component.dart通过 lib/layout.dart 导出RowComponent、ColumnComponent、ExpandedComponent、PaddingComponent目前属于实验性 API实现在 src/experimental/ 目录下通过 lib/experimental.dart 导出。按照 experimental.dart 库文件的说明实验性子模块中的 API 可能仍不完整、演进速度比主库更快但官方鼓励社区使用它们进行beta 测试待组件成熟后会迁入主库。因此使用这些组件时需要import package:flame/experimental.dart;AlignComponent只需import package:flame/components.dart;。整个布局体系建立在两个抽象基类之上理解它们就能理解所有具体组件的行为LayoutComponent所有布局组件的共同基类继承自PositionComponent定义布局尺寸与固有尺寸intrinsic size两个核心概念并约定子类必须实现layoutChildren()和intrinsicSize。LinearLayoutComponent线性布局Row/Column的公共超类实现主轴/交叉轴对齐、gap 间距、shrink-wrap、Expanded 均分等全部逻辑。基座LayoutComponent 与尺寸模型src/experimental/layout_component.dart 定义了LayoutComponent抽象类与LayoutAxis枚举x(0)/y(1)其axisIndex可直接用于Vector2索引取值。布局尺寸layoutSize与固有尺寸intrinsicSize布局组件把我有多大与我需要多大分开管理layoutSizeX/layoutSizeY显式指定的布局尺寸为null表示该轴未指定走 shrink-wrapintrinsicSize一个抽象 getter返回在给定约束下这个容器能拥有的最小尺寸对线性布局而言即子组件沿主轴之和、沿交叉轴的最大值见下文源码resetSize()把size重置为layoutSize与intrinsicSize的组合——显式指定的轴用布局尺寸未指定的轴用固有尺寸。两个设置尺寸的方法setLayoutSize(double? x, double? y)同时设置两个轴并一次性触发resetSize()。源码注释特别强调它不等价于对两个轴分别调用setLayoutAxisLength——后者会触发两次尺寸监听。setLayoutAxisLength(LayoutAxis axis, double? value)只设置单个轴的布局尺寸传null会把该轴设为固有尺寸。例如setLayoutAxisLength(LayoutAxis.y, 100)把高度固定为 100。布局刷新机制onChildrenChanged会在子组件增删时自动为子组件仅限PositionComponent注册/注销size监听器并触发layoutChildren()因此当子组件尺寸变化时父布局会自动重新计算。这正是声明式体验的关键你只需要改子组件或父组件的尺寸/属性布局会自动刷新。isShrinkWrappedIn(LayoutAxis axis)用于判断某个轴是否处于 shrink-wrap 状态即该轴layoutSize null。线性布局核心LinearLayoutComponent 与 Directionsrc/experimental/linear_layout_component.dart 是 Row 与 Column 的公共实现约 490 行代码承载了全部布局算法。Direction 枚举与主轴/交叉轴Direction枚举有两个值horizontal与vertical并提供了三个便捷方法mainAxis主轴——水平布局对应LayoutAxis.x垂直布局对应LayoutAxis.ycrossAxis交叉轴——与主轴正交的方向mainAxisValue(Vector2)/crossAxisValue(Vector2)取出向量在主轴/交叉轴上的数值。触发重新布局的条件源码注释明确列出了LinearLayoutComponent触发 re-layout 的全部情形子组件增删、部分子组件尺寸变化、gap变化、size变化、mainAxisAlignment变化、crossAxisAlignment变化。onMount时组件自身还会监听size变化size.addListener(layoutChildren)onRemove时移除监听。参数与默认值来自 RowComponent/ColumnComponent 构造函数RowComponent与ColumnComponent构造参数完全一致仅direction不同参数默认值说明mainAxisAlignmentMainAxisAlignment.start主轴对齐方式crossAxisAlignmentCrossAxisAlignment.start交叉轴对齐方式gap0.0相邻子组件间距sizenull显式尺寸null表示 shrink-wrapposition—组件位置anchor—锚点priority—渲染优先级childrenconst []子组件列表LinearLayoutComponent.fromDirection(Direction, ...)工厂方法可按方向一键创建Direction.horizontal返回RowComponentDirection.vertical返回ColumnComponent。基本用法来自 row_component.dart 的官方示例RowComponent( gap: 10.0, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ TextComponent(Child 1), TextComponent(Child 2), TextComponent(Child 3), ], );ColumnComponent用法完全相同只是方向变为垂直。这两个类本身几乎不含逻辑——RowComponent只有构造器其余全部继承自LinearLayoutComponent。对齐与 gap 的完整语义_layoutMainAxis()中的实现可以精确说明各对齐方式的行为spaceEvenly时首尾各留一个 gap 且间隙均分spaceAround时每个子组件两侧各留半个 gapspaceBetween首尾贴边、中间均分start/end/center则把剩余空间分别放在尾部、头部、两侧。gap是一个智能 getter见源码 linear_layout_component.dart它会在以下情况被空间对齐方式覆盖当mainAxisAlignment为spaceAround/spaceBetween/spaceEvenly时gap由剩余空间均分计算得出显式设置的gap被忽略一旦子组件中存在ExpandedComponent上述三种对齐自动失效间隙完全由gap决定因为 Expanded 会吃掉所有剩余空间。shrink-wrap 模式的行为重要陷阱把size设为null即进入 shrink-wrap布局组件不再自上而下地确定尺寸而是通过intrinsicSize从子组件推导自身尺寸。在此模式下源码明确说明以下几点行为mainAxisAlignment无论设成什么都表现为MainAxisAlignment.start该 getter 在 shrink-wrap 时直接返回startcrossAxisAlignment会让所有子组件沿交叉轴拥有相同长度等于该轴最大的子组件ExpandedComponent不再展开其尺寸退化为intrinsicSize即它自己的子组件尺寸。intrinsicSize的计算源码 linear_layout_component.dart主轴方向为所有子组件主轴尺寸之和 (n-1)×gap交叉轴方向为所有子组件交叉轴尺寸的最大值没有子组件时返回Vector2.zero()。CrossAxisAlignment 的边界行为CrossAxisAlignment.baseline目前不被支持行为等同于start源码第 64-65 行明确注释CrossAxisAlignment.stretch会永久性地修改子组件尺寸由于PositionComponent没有统一的固有尺寸接口stretch 会把子组件沿交叉轴的尺寸直接改写为容器的交叉轴长度之后再次修改crossAxisAlignment将基于新的子尺寸工作。对于垂直方向的ColumnComponentstretch 还会把子TextBoxComponent的boxConfig.maxWidth设为容器宽度以保证文本自动换行见_crossAxisSizing中的特判逻辑。单子布局基类SingleLayoutComponentsrc/experimental/single_layout_component.dart 为只管理一个子组件的布局组件ExpandedComponent、PaddingComponent提供公共实现未来也可能用来重构AlignComponent源码注释如此说明。childsetter 会自动移除旧子组件、挂载新子组件inflateChild标志决定布局机制是否改写子组件尺寸为true时syncChildSize()会把子组件尺寸同步为availableSize若子组件是LayoutComponent则调用其setLayoutSizeintrinsicSize默认实现为child?.size ?? Vector2.zero()。ExpandedComponent吃掉主轴的剩余空间src/experimental/expanded_component.dart 与 Flutter 的Expandedwidget 行为类似但有硬性约束必须是LinearLayoutComponent的直接子组件通过ParentIsALinearLayoutComponentmixin 保证它自身从不 shrink-wrap只向父组件报告intrinsicSize并从父组件接收尺寸信息当父组件在主轴上 shrink-wrap 时它不会被展开inflateChild默认true当父组件把展开后的长度赋给它时它会同步把子组件的对应轴尺寸也设置为该值。官方示例来自源码注释ColumnComponent( children: [ ExpandedComponent( child: TextComponent(text: foo), ), TextComponent(text: bar), ], );ExpandedComponent.layoutChildren()的实现是resetSize()后调用parent.layoutChildren()即主动触发父布局重排。展开的具体算法在LinearLayoutComponent._mainAxisSizing将所有 Expanded 子组件平分剩余空间freeSpace / expandedComponents.length并且只在数值变化时才调用setLayoutAxisLength以避免无谓的监听器触发。PaddingComponent为子组件添加内边距src/experimental/padding_component.dart 对应 Flutter 的PaddingwidgetPaddingComponent( padding: EdgeInsets.all(10), child: TextComponent(text: bar), );关键行为使用与 Flutter 相同的EdgeInsets来自package:flutter/rendering.dart默认EdgeInsets.zero该组件设计为仅容纳一个子组件请用child参数设置子组件源码明确警告避免直接在实例上调用add多子组件行为未定义尺寸既可按子组件 shrink-wrapintrinsicSize 子组件尺寸 padding.horizontal/vertical也可显式设置size子组件只会被 padding 偏移padding与child都可以事后修改修改会触发布局刷新paddingsetter 调用layoutChildren()availableSize实现为padding.deflateSize(size.toSize())即容器尺寸扣除 padding 后的可用区域当inflateChild为true时子组件会被拉伸填满该区域默认inflateChild false。AlignComponent父容器内的相对定位AlignComponent与 Flutter 的Alignwidget 类似实现于 src/layout/align_component.dart是布局体系中唯一属于稳定主库的组件。核心参数参数默认值说明childnull被对齐的子组件只对齐初始设置的这一个 childalignmentAnchor.topLeft子组件在父容器内的对齐位置widthFactornull非 null 时组件宽度 子组件宽度 × 该因子heightFactornull非 null 时组件高度 子组件高度 × 该因子keepChildAnchorfalsefalse时子组件的 anchor 会被强制同步为 alignment 值基本用法AlignComponent( child: TextComponent(hello), alignment: Anchor.centerLeft, );alignment使用 Flame 的Anchor而非 Flutter 的Alignment。源码特别说明了两者坐标系差异Flame 中组件左上角相对坐标为(0, 0)右下角为(1, 1)这与 Flutter 的Alignment不同。子组件的位置由_updateChildPosition计算position Vector2(size.x * alignment.x, size.y * alignment.y)。尺寸规则正常情况下组件尺寸等于父容器尺寸但如果提供了widthFactor/heightFactor对应方向改为子组件尺寸 × 因子。例如heightFactor 1时高度跟随子组件、宽度仍等于父容器宽度。size不能被直接设置——setter 直接抛出UnsupportedError(The size of AlignComponent cannot be set directly)尺寸由onParentResize统一计算。keepChildAnchor 的进阶用法默认keepChildAnchor false子组件 anchor 会被强制设为 alignment 值从而获得传统对齐效果子组件中心放在容器中心、子组件右下角放在容器右下角等。若保持子组件自身 anchor 并设置keepChildAnchor: true可以实现越界摆放——官方示例把子组件放在父组件上方PlayerSprite().add( AlignComponent( child: HealthBar()..anchor Anchor.bottomCenter, alignment: Anchor.topCenter, keepChildAnchor: true, ), );此时HealthBar的底部中心被对齐到PlayerSprite的顶部中心相当于把血条顶到角色头顶上方——这是实现角色头顶血条、名称标签等效果的惯用手法。此外onMount中有一个断言AlignComponent的父组件必须是有尺寸的ReadOnlySizeProvider因此把它直接挂在无尺寸的组件下会触发断言失败。源码验证测试用例如何锁定布局行为仓库中的测试可以直接作为行为契约来研读test/experimental/linear_layout_component_test.dart共 518 行系统性地覆盖了mainAxisAlignment的start/end/center/spaceBetween等对齐在水平与垂直两个方向上的位置计算。例如center用例断言首子组件偏移 (容器主轴尺寸 − 子组件占用 − gap 空间) / 2spaceBetween用例断言layoutComponent.gap等于 (容器尺寸 − 占用) / (n−1)与上述 gap 智能覆盖逻辑完全吻合test/experimental/padding_component_test.dart 验证 padding 对子组件位置与自身尺寸的影响test/layout/align_component_test.dart 验证AlignComponent的对齐、因子与 anchor 行为。这些测试与 linear_layout_component_test_helpers.dart 配合用真实游戏实例game.ensureAdd驱动布局再断言子组件的position/size可作为复现与调参时的参考基准。组合实战构建一个自适应 HUD综合以上全部组件一个典型的自适应 HUD 可以这样组织示意结合上文各组件行为class HUD extends Component { override Futurevoid onLoad() async { add( AlignComponent( alignment: Anchor.topCenter, // 固定在屏幕顶部居中 child: PaddingComponent( padding: const EdgeInsets.all(12), child: RowComponent( gap: 8, mainAxisAlignment: MainAxisAlignment.center, children: [ TextComponent(HP:), ExpandedComponent(child: BarComponent()), // 自动撑满剩余宽度 TextComponent(100/100), ], ), ), ), ); } }这段代码同时用到了AlignComponent顶部居中、PaddingComponent四周留白、RowComponent水平排列、ExpandedComponent血条填充剩余宽度且全部自动响应父容器与子组件尺寸变化——这正是声明式布局相比手算坐标的核心价值。使用注意与限制汇总实验性 APIRow/Column/Expanded/Padding 均标记Warning: Experimental. API and behavior may change.升级 Flame 版本时需关注 CHANGELOG.mdExpanded 必须是线性布局的直接子级且其存在会使spaceAround/spaceBetween/spaceEvenly自动失效CrossAxisAlignment.baseline不支持等同startstretch会永久改写子组件尺寸shrink-wrapsize 为 null时主轴对齐退化为startExpanded 不再展开AlignComponent的 size 不可直接设置且父组件必须提供尺寸它只对齐初始的child后续add的其他子组件不会被对齐PaddingComponent只支持一个子组件请勿直接add多个子组件。需要深入研究行为细节时建议直接阅读 linear_layout_component.dart、layout_component.dart 与 align_component.dart 的源码注释其中包含了大量属性相互作用与陷阱Property interactions and gotchas的权威说明是理解 Flame 布局行为的一手资料。【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考