Skip to content

尺寸单位与响应式

Listing 28-1 到 28-4 都用 Val::Px 硬编码尺寸——在固定分辨率下没问题,但窗口缩放或换到不同分辨率的屏幕上就会溢出或留白。Val 的六种单位正是为了解决这个问题。

Percent:相对于父容器

Val::Percent(50.0) 意味着"父容器宽度的 50%"。这是最常用的响应式单位——父容器变大,子节点自动跟着变。

rust
Node {
    width: Val::Percent(80.0),   // 父容器宽度的 80%
    height: Val::Percent(100.0), // 和父容器一样高
    ..default()
}

注意:Percent 的参照物是父容器,不是窗口。嵌套层级越深,参照物越小。

Vw / Vh:相对于视口

Val::Vw(n) 是窗口宽度的 n%,Val::Vh(n) 是窗口高度的 n%。无论嵌套多深,参照物始终是窗口本身:

rust
Node {
    width: Val::Vw(28.0),  // 窗口宽度的 28%
    height: Val::Vh(5.0),  // 窗口高度的 5%
    ..default()
}

这对 HUD 元素(血条、地图、小地图)特别有用——你希望它们始终占屏幕的固定比例,不管 UI 树嵌套多深。

VMin / VMax:相对于较短/较长边

Val::VMin(30.0) 是窗口较短边的 30%。横屏时参照高度,竖屏时参照宽度。适合需要在不同屏幕方向下保持正方形的元素。

嵌套布局实战

下面的例子把前面学到的 Flexbox 和响应式单位结合起来,构建一个典型的"头-体-脚"三段式布局:

rust
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);
    commands
        .spawn((
            Node {
                width: Val::Percent(100.0),
                height: Val::Percent(100.0),
                flex_direction: FlexDirection::Column,
                padding: UiRect::all(Val::Px(16.0)),
                row_gap: Val::Px(16.0),
                ..default()
            },
            BackgroundColor(Color::srgb(0.12, 0.12, 0.12)),
        ))
        .with_children(|parent| {
            // Header
            parent.spawn((
                Node {
                    width: Val::Percent(100.0),
                    height: Val::Px(60.0),
                    justify_content: JustifyContent::Center,
                    align_items: AlignItems::Center,
                    ..default()
                },
                BackgroundColor(Color::srgb(0.25, 0.35, 0.55)),
            ));

            // Body: sidebar + content
            parent
                .spawn(Node {
                    width: Val::Percent(100.0),
                    flex_grow: 1.0,
                    flex_direction: FlexDirection::Row,
                    column_gap: Val::Px(16.0),
                    ..default()
                })
                .with_children(|parent| {
                    // Sidebar
                    parent.spawn((
                        Node {
                            width: Val::Px(180.0),
                            height: Val::Percent(100.0),
                            flex_direction: FlexDirection::Column,
                            padding: UiRect::all(Val::Px(12.0)),
                            row_gap: Val::Px(8.0),
                            ..default()
                        },
                        BackgroundColor(Color::srgb(0.2, 0.2, 0.25)),
                    ));

                    // Content area with nested card
                    parent
                        .spawn((
                            Node {
                                flex_grow: 1.0,
                                height: Val::Percent(100.0),
                                padding: UiRect::all(Val::Px(16.0)),
                                ..default()
                            },
                            BackgroundColor(Color::srgb(0.18, 0.18, 0.22)),
                        ))
                        .with_children(|parent| {
                            parent.spawn((
                                Node {
                                    width: Val::Px(200.0),
                                    height: Val::Px(120.0),
                                    border_radius: BorderRadius::all(Val::Px(12.0)),
                                    ..default()
                                },
                                BackgroundColor(Color::srgb(0.35, 0.5, 0.7)),
                            ));
                        });
                });

            // Footer
            parent.spawn((
                Node {
                    width: Val::Percent(100.0),
                    height: Val::Px(40.0),
                    justify_content: JustifyContent::Center,
                    align_items: AlignItems::Center,
                    ..default()
                },
                BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
            ));
        });
}

Listing 28-5:嵌套布局——Header / Sidebar+Content / Footer(examples/listing-28-05.rs)

几个关键点:

  • flex_grow: 1.0——Body 区域和 Content 区域都设了 flex_grow,让它们吃掉剩余空间。Header 和 Footer 固定高度,Body 自动填满中间。
  • flex_direction: FlexDirection::Row——Body 内部是水平排列:Sidebar 固定 180px 宽,Content 用 flex_grow 填满。
  • Sidebar 用 flex_direction: FlexDirection::Column 准备好垂直排列菜单项(这里只留了空容器)。

Grid 布局

当需要二维排列(行和列同时控制)时,用 Display::Grid 替代默认的 Display::Flex。Grid 布局通过 grid_template_columnsgrid_template_rows 定义轨道:

rust
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);
    commands
        .spawn((
            Node {
                width: Val::Percent(100.0),
                height: Val::Percent(100.0),
                display: Display::Grid,
                grid_template_columns: vec![
                    GridTrack::px(200.0),
                    GridTrack::flex(1.0),
                    GridTrack::px(150.0),
                ],
                grid_template_rows: vec![
                    GridTrack::px(60.0),
                    GridTrack::flex(1.0),
                    GridTrack::px(40.0),
                ],
                row_gap: Val::Px(8.0),
                column_gap: Val::Px(8.0),
                padding: UiRect::all(Val::Px(8.0)),
                ..default()
            },
            BackgroundColor(Color::srgb(0.1, 0.1, 0.1)),
        ))
        .with_children(|parent| {
            // Header: spans all 3 columns
            parent.spawn((
                Node {
                    grid_column: GridPlacement::span(3),
                    ..default()
                },
                BackgroundColor(Color::srgb(0.25, 0.35, 0.55)),
            ));

            // Left sidebar
            parent.spawn((
                Node {
                    ..default()
                },
                BackgroundColor(Color::srgb(0.2, 0.2, 0.25)),
            ));

            // Main content (auto-placed in center)
            parent.spawn((
                Node {
                    ..default()
                },
                BackgroundColor(Color::srgb(0.18, 0.18, 0.22)),
            ));

            // Right sidebar
            parent.spawn((
                Node {
                    ..default()
                },
                BackgroundColor(Color::srgb(0.22, 0.22, 0.28)),
            ));

            // Footer: spans all 3 columns
            parent.spawn((
                Node {
                    grid_column: GridPlacement::span(3),
                    ..default()
                },
                BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
            ));
        });
}

Listing 28-6:CSS Grid——三行三列的经典仪表盘布局(examples/listing-28-06.rs)

GridTrack 提供几种轨道定义:

方法含义
GridTrack::px(200.0)固定 200px
GridTrack::flex(1.0)minmax(0, 1fr)——弹性分配剩余空间
GridTrack::fr(1.0)minmax(auto, 1fr)——内容决定最小尺寸
GridTrack::auto()由内容决定
GridTrack::percent(30.0)父容器的 30%

RepeatedGridTrack::flex(4, 1.0)GridTrack::flex(1.0) 重复 4 次的简写。

grid_column: GridPlacement::span(3) 让一个格子横跨 3 列——Header 和 Footer 就是这么铺满整行的。

何时用 Grid,何时用 Flex

  • 一维排列(水平或垂直的一行/一列)→ Flex
  • 二维排列(同时控制行和列)→ Grid
  • 嵌套混合很常见:外层 Grid 划分大区域,内层 Flex 排列具体内容

小结:Percent 相对于父容器,Vw/Vh 相对于窗口,VMin/VMax 相对于窗口较短/较长边。flex_grow 让子节点分配剩余空间。Grid 布局用 grid_template_columns/rows 定义二维轨道。实际项目中 Flex 和 Grid 经常嵌套使用。