# 第六章:布局系统:构建响应式 UI
> **本章导读**:想象你是一位室内设计师,面对着形态各异的房间——有的是狭长的走廊,有的是宽敞的大厅,有的是紧凑的阁楼。你的设计需要在所有这些空间中都显得和谐得体。在 Uno Platform 开发中,你就是那位设计师,屏幕就是你的房间。从 4 英寸的手机到 27 英寸的显示器,从竖屏到横屏,从折叠屏到分屏——你的 UI 必须优雅地适应所有场景。本章将揭示布局的奥秘,让你的应用在任何屏幕上都如鱼得水。
---
## 🏛️ 6.1 布局哲学的根本转变
在深入具体技术之前,让我们先理解布局的哲学。不同的 UI 框架有不同的布局哲学,理解这种差异能帮助你更快地掌握 XAML 布局系统。
### 🌊 6.1.1 三种布局范式
**流式布局**(Flow Layout)是 Web 开发的传统方式。元素像水流一样从左到右、从上到下排列,当空间不足时自动换行。这种布局灵活但控制力较弱。
**约束布局**(Constraint Layout)是 iOS Auto Layout 和 Android ConstraintLayout 的方式。你定义元素之间的关系——"A 在 B 的右边,距离 8 像素"——系统自动计算位置。这种方式灵活且精确,但复杂布局的关系网络会变得难以理解。
**容器布局**(Container Layout)是 XAML 的方式。每个元素生活在一个容器中,容器决定子元素的大小和位置。不同的容器有不同的排列规则:`Grid` 用行列划分,`StackPanel` 按顺序堆叠,`Canvas` 用绝对坐标。
> **第一性原理**:为什么 XAML 选择容器模型?
>
> 容器模型的优势在于**组合性**。复杂的布局可以通过嵌套简单容器来实现,每个容器只关心自己的职责。这种分而治之的思想使得布局逻辑清晰可维护。虽然嵌套层级可能较深,但每一层的语义都是明确的。
### 📐 6.1.2 布局的测量-排列协议
所有布局容器都遵循一个统一的**测量-排列协议**(Measure-Arrange Protocol)。理解这个协议是掌握布局调试的关键。
**测量阶段**(Measure):父容器询问每个子元素"你需要多大的空间?"。子元素根据自身内容返回一个"期望大小"(Desired Size)。
**排列阶段**(Arrange):父容器根据可用空间和子元素的期望大小,决定每个子元素的最终位置和尺寸,并告诉子元素"你的位置和大小是这些"。
这个两阶段过程从可视化树的根节点开始,递归向下进行。子元素的测量结果会影响父元素的测量结果,而父元素的排列决定会影响子元素的最终显示。
> **费曼技巧提问**:为什么需要两个阶段而不是一个?
>
> 想象你在安排一场宴会的座位。如果只问每个人"你想坐哪",可能会出现冲突(两人都要靠窗的位置)。两个阶段的设计允许你先收集所有人的需求(测量),然后在全局视角下做出最优安排(排列)。这就是为什么容器能在空间不足时"压缩"子元素,或者在有富余空间时"扩展"它们。
---
## 🏆 6.2 核心布局容器详解
XAML 提供了多种布局容器,每种都有其适用场景。让我们逐一深入。
### 📊 6.2.1 Grid:布局之王
`Grid` 是最强大、最常用的布局容器。它将空间划分为行和列的网格,每个子元素可以跨越一个或多个单元格。
```xml
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="2*" />
<RowDefinition Height="48" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- 标题行,横跨三列 -->
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"
Text="应用标题" FontSize="24" />
<!-- 侧边栏 -->
<ListView Grid.Row="1" Grid.Column="0" Grid.RowSpan="2" />
<!-- 主内容区 -->
<ScrollViewer Grid.Row="1" Grid.Column="1">
<TextBlock Text="主要内容" />
</ScrollViewer>
<!-- 辅助面板 -->
<StackPanel Grid.Row="1" Grid.Column="2">
<TextBlock Text="详细信息" />
</StackPanel>
<!-- 底部工具栏 -->
<StackPanel Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3"
Orientation="Horizontal">
<Button Content="确定" />
<Button Content="取消" />
</StackPanel>
</Grid>
```
**行高和列宽的四种设置方式**:
**固定值**(如 `48`):精确指定像素值。在某些场景下有用,但会降低响应式能力。
**Auto**:大小由内容决定。如果单元格中是一个文本块,行高就是文本的高度。
**星号**(`*`):按比例分配剩余空间。如果有两行分别是 `*` 和 `2*`,它们会按 1:2 的比例分配剩余空间。
**混合使用**:可以在同一个 Grid 中混合使用不同的单位。计算顺序是:先满足固定值和 Auto,剩下的空间按星号比例分配。
> **技术细节**:Grid 的性能考量
>
> `Grid` 需要计算所有行列的尺寸,有一定的计算开销。对于简单的顺序排列,`StackPanel` 性能更好。但对于大多数应用界面,`Grid` 的灵活性远超其性能开销。性能问题通常出现在过度嵌套而非容器选择本身。
### 📚 6.2.2 StackPanel:简单而高效
`StackPanel` 将子元素按水平或垂直方向顺序排列,不换行、不换列。它是最简单的布局容器,适合简单的线性布局。
```xml
<!-- 垂直堆叠(默认) -->
<StackPanel Orientation="Vertical" Spacing="8">
<TextBlock Text="标题" FontSize="24" />
<TextBlock Text="副标题" FontSize="16" />
<Button Content="操作按钮" />
</StackPanel>
<!-- 水平堆叠 -->
<StackPanel Orientation="Horizontal" Spacing="12">
<SymbolIcon Symbol="Accept" />
<TextBlock Text="操作成功" VerticalAlignment="Center" />
</StackPanel>
```
`Spacing` 属性是 WinUI 3 新增的便利属性,自动在所有子元素之间添加均匀的间距,省去了为每个元素设置 `Margin` 的麻烦。
**StackPanel 的陷阱**:它给子元素提供**无限的**可用空间(在堆叠方向上)。这意味着如果一个可滚动控件(如 `ScrollViewer`)放在 `StackPanel` 中,它可能会无限扩展而不显示滚动条。解决方案是限制 `ScrollViewer` 的高度,或者使用 `Grid` 代替。
```xml
<!-- 错误:ScrollViewer 可能无限扩展 -->
<StackPanel>
<TextBlock Text="标题" />
<ScrollViewer>
<TextBlock Text="很长的内容..." />
</ScrollViewer>
</StackPanel>
<!-- 正确:用 Grid 限制 ScrollViewer 的高度 -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="标题" />
<ScrollViewer Grid.Row="1">
<TextBlock Text="很长的内容..." />
</ScrollViewer>
</Grid>
```
### 🧭 6.2.3 RelativePanel:相对定位的艺术
`RelativePanel` 允许子元素相对于其他元素或面板边界进行定位。它特别适合需要复杂相对关系但又不想使用多层嵌套的场景。
```xml
<RelativePanel Padding="16">
<TextBox x:Name="EmailField"
Header="邮箱"
RelativePanel.AlignLeftWithPanel="True"
RelativePanel.AlignRightWithPanel="True" />
<TextBox x:Name="PasswordField"
Header="密码"
RelativePanel.AlignLeftWithPanel="True"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.Below="EmailField" />
<Button x:Name="LoginButton"
Content="登录"
RelativePanel.Below="PasswordField"
RelativePanel.AlignHorizontalCenterWithPanel="True" />
<HyperlinkButton Content="忘记密码?"
RelativePanel.AlignVerticalCenterWith="LoginButton"
RelativePanel.RightOf="LoginButton" />
</RelativePanel>
```
**RelativePanel 的附加属性**:
**对齐到面板**:`AlignLeftWithPanel`、`AlignTopWithPanel`、`AlignRightWithPanel`、`AlignBottomWithPanel`、`AlignHorizontalCenterWithPanel`、`AlignVerticalCenterWithPanel`。
**相对于其他元素**:`Above`、`Below`、`LeftOf`、`RightOf`、`AlignHorizontalCenterWith`、`AlignVerticalCenterWith`、`AlignLeftWith`、`AlignTopWith`、`AlignRightWith`、`AlignBottomWith`。
> **设计提示**:RelativePanel vs Grid
>
> 如果布局有清晰的行列结构,用 `Grid` 更直观。如果布局需要复杂的相对关系(如"A 在 B 右边,C 在 A 下面,D 和 C 垂直居中"),`RelativePanel` 会更简洁。两者可以嵌套使用,在 `Grid` 的单元格中使用 `RelativePanel` 是常见模式。
### 🎨 6.2.4 Canvas:绝对定位的利器
`Canvas` 使用绝对坐标定位子元素,不进行任何自动布局。它在大多数应用界面中用得较少,但在特定场景下不可或缺。
```xml
<Canvas Width="400" Height="300">
<Rectangle Canvas.Left="50" Canvas.Top="50"
Width="100" Height="100"
Fill="Blue" />
<Ellipse Canvas.Left="200" Canvas.Top="100"
Width="80" Height="80"
Fill="Red" />
<TextBlock Canvas.Left="10" Canvas.Bottom="10"
Text="画布内容" />
</Canvas>
```
**适用场景**:绘图应用、游戏界面、需要精确像素控制的场景、叠加层(如水印、徽章)。
**注意事项**:`Canvas` 中的元素不会被自动裁剪,超出画布边界的部分仍然可见。此外,`Canvas` 不会自动调整子元素大小,每个元素必须显式设置尺寸或依赖自身内容的尺寸。
### 🔀 6.2.5 其他实用容器
**VariableSizedWrapGrid**:类似于 `StackPanel`,但在空间不足时自动换行。适合创建网格状的项列表。
```xml
<VariableSizedWrapGrid Orientation="Horizontal"
ItemWidth="100" ItemHeight="100"
MaximumRowsOrColumns="4">
<!-- 每个项 100x100,最多 4 列,自动换行 -->
</VariableSizedWrapGrid>
```
**UniformGrid**:所有单元格大小相等的网格,不需要定义行列。
```xml
<UniformGrid Rows="3" Columns="3">
<!-- 9 个等大的单元格 -->
</UniformGrid>
```
**Border**:虽然不是布局容器,但它可以包装单个子元素并添加边框和背景。
```xml
<Border BorderBrush="Gray" BorderThickness="1"
Background="LightGray" CornerRadius="8" Padding="16">
<TextBlock Text="带边框的内容" />
</Border>
```
---
## 📱 6.3 响应式设计:断点与状态管理
现代应用需要在从手机到桌面的各种设备上运行。XAML 提供了强大的响应式设计工具。
### 🔄 6.3.1 VisualStateManager:状态驱动的响应式
`VisualStateManager`(VSM)是响应式设计的核心机制。它允许你定义不同的视觉状态,以及触发状态切换的条件。
```xml
<Page>
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<!-- 窄屏状态(手机竖屏) -->
<VisualState x:Name="NarrowState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ContentGrid.Visibility" Value="Collapsed" />
<Setter Target="NavStackPanel.Orientation" Value="Horizontal" />
<Setter Target="MainGrid.Margin" Value="12" />
</VisualState.Setters>
</VisualState>
<!-- 中等宽度状态(平板竖屏、手机横屏) -->
<VisualState x:Name="MediumState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="641" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ContentGrid.Visibility" Value="Visible" />
<Setter Target="NavStackPanel.Orientation" Value="Vertical" />
<Setter Target="MainGrid.Margin" Value="24" />
</VisualState.Setters>
</VisualState>
<!-- 宽屏状态(桌面、平板横屏) -->
<VisualState x:Name="WideState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="1008" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ContentGrid.Visibility" Value="Visible" />
<Setter Target="NavStackPanel.Orientation" Value="Vertical" />
<Setter Target="MainGrid.Margin" Value="40" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid x:Name="MainGrid">
<!-- 页面内容 -->
</Grid>
</Grid>
</Page>
```
**断点的选择**:常见的断点值参考了 Windows 11 的设计指南:
- 0-640 像素:手机竖屏
- 641-1007 像素:平板竖屏、手机横屏
- 1008+ 像素:桌面、平板横屏
> **费曼技巧提问**:为什么是这些特定的数值?
>
> 这些数值不是随意选择的。640 像素是历史上许多手机屏幕的宽度;1008 像素则来自于 1024 减去典型的窗口边框和边距。更重要的是,这些断点对应了用户体验的质变点:在 640 像素以下,内容通常需要单列显示;超过 1008 像素,就可以显示侧边栏等辅助内容。
### 📏 6.3.2 自定义状态触发器
除了 `AdaptiveTrigger`,你还可以创建自定义的状态触发器。Uno Platform 提供了一些额外的触发器:
```xml
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="DesktopState">
<VisualState.StateTriggers>
<platform:PlatformTrigger Platform="WinUI" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="InstallButton.Content" Value="下载 Windows 版" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="MobileState">
<VisualState.StateTriggers>
<platform:PlatformTrigger Platform="Android,iOS" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="InstallButton.Content" Value="前往应用商店" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
```
### 🔢 6.3.3 x:Load 与条件加载
WinUI 3 引入了 `x:Load` 属性,允许在特定条件下加载或卸载元素。这在响应式设计中非常有用——你可以在窄屏时完全移除某些元素,而不是仅仅隐藏它们。
```xml
<Grid>
<!-- 窄屏时这个面板根本不会被加载到内存中 -->
<StackPanel x:Name="WideOnlyPanel" x:Load="False">
<TextBlock Text="这只在宽屏时显示" />
</StackPanel>
</Grid>
```
然后在代码后置或状态触发器中控制:
```csharp
// 在 VisualState 的 Setter 中
<Setter Target="WideOnlyPanel.(x:Load)" Value="True" />
```
> **技术要点**:x:Load vs Visibility
>
> `Visibility="Collapsed"` 的元素仍然存在于可视化树中,占用内存并参与布局测量。`x:Load="False"` 的元素完全不存在于树中,没有任何开销。但切换 `x:Load` 会触发元素的重新创建,状态会丢失。对于频繁切换的场景,用 `Visibility`;对于根据屏幕尺寸一次性决定的场景,用 `x:Load`。
---
## 🔍 6.4 深入测量与排列:对齐与边距
理解对齐(Alignment)和边距(Margin)的工作原理,能帮助你避免许多布局陷阱。
### ⬛ 6.4.1 HorizontalAlignment 与 VerticalAlignment
对齐属性决定了元素在分配给它的空间内的位置。
```xml
<Grid Width="300" Height="200" Background="LightGray">
<!-- 左上角 -->
<Button Content="左上"
HorizontalAlignment="Left" VerticalAlignment="Top"
Width="80" Height="30" />
<!-- 居中 -->
<Button Content="居中"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="80" Height="30" />
<!-- 拉伸(默认值) -->
<Button Content="拉伸"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Margin="0,100,0,0" />
</Grid>
```
**Stretch 是默认值**:对于大多数控件,`HorizontalAlignment` 和 `VerticalAlignment` 的默认值都是 `Stretch`。这意味着如果父容器给了足够的空间,控件会自动扩展填满。
**覆盖 Stretch 的因素**:显式设置 `Width` 或 `Height` 会覆盖 `Stretch` 行为。如果想让控件拉伸但保持比例,可以只设置一个维度。
### 📐 6.4.2 Margin 与 Padding
`Margin` 定义元素外部的空间,`Padding` 定义元素内部的空间。
```xml
<!-- Margin: 外边距 -->
<Border Background="Blue" Margin="10,20,10,20">
<!-- Padding: 内边距 -->
<StackPanel Background="White" Padding="16">
<TextBlock Text="内容" />
</StackPanel>
</Border>
```
**Margin 的简写语法**:
- `Margin="10"`:四边都是 10
- `Margin="10,20"`:左右 10,上下 20
- `Margin="10,20,30,40"`:左、上、右、下(顺时针方向)
> **设计技巧**:使用 Spacing 代替 Margin
>
> 在容器中,与其给每个子元素设置 `Margin`,不如使用容器的 `Spacing` 属性(如果支持)。`Grid`、`StackPanel`、`RelativePanel` 都支持 `Spacing`,代码更简洁,语义更清晰。
---
## 🔄 6.5 滚动与视图缩放
当内容超出可视区域时,需要滚动机制;当内容需要整体缩放时,需要视图缩放机制。
### 📜 6.5.1 ScrollViewer:滚动容器
`ScrollViewer` 为其内容提供滚动能力。它可以处理垂直、水平或双向滚动。
```xml
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollMode="Enabled"
ZoomMode="Disabled">
<StackPanel Spacing="16">
<!-- 很多内容... -->
</StackPanel>
</ScrollViewer>
```
**常用属性**:
- `VerticalScrollBarVisibility` / `HorizontalScrollBarVisibility`:滚动条的可见性(`Auto`、`Visible`、`Hidden`、`Disabled`)
- `VerticalScrollMode` / `HorizontalScrollMode`:滚动模式(`Enabled`、`Disabled`、`Auto`)
- `ZoomMode`:是否允许缩放(`Enabled`、`Disabled`)
- `IsVerticalRailEnabled` / `IsHorizontalRailEnabled`:是否启用滚动轨道(用于精确滚动)
**滚动到特定元素**:
```csharp
// 滚动使特定元素可见
MyScrollViewer.ScrollToVerticalOffset(targetElement.Offset.Y);
// 或使用 ChangeView 方法(更现代的 API)
MyScrollViewer.ChangeView(null, targetElement.Offset.Y, null);
```
### 📦 6.5.2 ViewBox:自动缩放
`ViewBox` 会根据可用空间自动缩放其内容,保持内容的纵横比。
```xml
<Viewbox Stretch="Uniform" MaxWidth="400" MaxHeight="300">
<!-- 这个 Grid 会按比例缩放以适应可用空间 -->
<Grid Width="200" Height="150">
<TextBlock Text="可缩放内容" FontSize="24" />
</Grid>
</Viewbox>
```
**Stretch 模式**:
- `None`:不缩放
- `Fill`:拉伸填满,可能变形
- `Uniform`:按比例缩放,完全适应(可能有留白)
- `UniformToFill`:按比例缩放,完全填满(可能裁剪)
---
## 🚀 6.6 高性能列表布局
当需要显示大量数据项时,传统的 `ListView` 可能性能不佳。WinUI 3 提供了 `ItemsRepeater`,一个高性能、可定制的列表控件。
### 📋 6.6.1 ItemsRepeater 基础
```xml
<ScrollViewer>
<ItemsRepeater ItemsSource="{x:Bind ViewModel.Items}"
VerticalAlignment="Top">
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="model:Item">
<Grid Padding="8">
<TextBlock Text="{x:Bind Title}" />
</Grid>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
<!-- 自定义布局 -->
<ItemsRepeater.Layout>
<StackLayout Spacing="8" />
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
```
### 🌊 6.6.2 使用不同的布局
`ItemsRepeater` 的强大之处在于可以轻松切换布局:
```xml
<!-- 均匀网格布局 -->
<ItemsRepeater.Layout>
<UniformGridLayout ItemsStretch="Fill"
MinItemWidth="120"
MinItemHeight="100" />
</ItemsRepeater.Layout>
<!-- 自定义流式布局 -->
<ItemsRepeater.Layout>
<FlowLayout LineAlignment="Start"
MinRowSpacing="8"
MinColumnSpacing="8" />
</ItemsRepeater.Layout>
```
### ⚡ 6.6.3 虚拟化
`ItemsRepeater` 内置了虚拟化支持——只渲染可见区域的项,大幅减少内存占用和渲染时间。当用户滚动时,项会被回收和重用,而不是重新创建。
> **性能提示**:ListView vs ItemsRepeater
>
> `ListView` 提供了内置的选择支持、分组功能和头部尾部模板,使用简单。`ItemsRepeater` 性能更高、更灵活,但需要自己实现选择等功能。如果列表项数量可能很大(数百或数千),优先考虑 `ItemsRepeater`。
---
## 🌐 6.7 平台特定布局微调
虽然目标是"一次编写,处处运行",但有时需要针对特定平台进行微调。
### 📱 6.7.1 OnPlatform 标记扩展
```xml
<StackPanel>
<!-- 不同平台使用不同的值 -->
<TextBlock FontSize="{OnPlatform Default=14, Android=16, iOS=16, WinUI=12}" />
<!-- 平台特定的属性设置 -->
<Button Content="分享">
<Button.Margin>
<OnPlatform x:TypeArguments="Thickness">
<On Platform="Android,iOS" Value="0,16,0,0" />
<On Platform="WinUI" Value="0,8,0,0" />
</OnPlatform>
</Button.Margin>
</Button>
</StackPanel>
```
### 📐 6.7.2 OnIdiom 标记扩展
`OnIdiom` 根据设备类型(手机、平板、桌面)提供不同的值:
```xml
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnIdiom Default=*, Desktop=240}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
```
---
## 📝 本章小结
布局系统是 UI 开发的基础,它决定了应用在各种屏幕上的呈现效果。本章我们从第一性原理出发,理解了 XAML 的容器模型和测量-排列协议。
我们深入学习了各种布局容器:`Grid` 的行列划分、`StackPanel` 的简单堆叠、`RelativePanel` 的相对定位、`Canvas` 的绝对坐标。每种容器都有其适用场景,选择正确的容器是构建高效布局的第一步。
响应式设计通过 `VisualStateManager` 和断点系统实现,让你的应用能够适应从手机到桌面的各种屏幕尺寸。`x:Load` 属性提供了更激进的优化手段,可以在不需要时完全卸载元素。
在下一章中,我们将从布局转向视觉美化——学习如何通过**资源字典、样式与模板**让应用从"能用"变得"精美"。当你掌握了这些技术,你的应用将拥有统一、专业的视觉风格,并且易于维护和定制。
---
> **动手实验**:
> 1. 使用 `Grid` 创建一个经典的应用布局:顶部标题栏、左侧导航栏、中间内容区、底部状态栏。尝试调整窗口大小,观察各区域的行为。
> 2. 使用 `VisualStateManager` 实现一个响应式界面:在窄屏时导航栏变成汉堡菜单,在宽屏时显示为侧边栏。
> 3. 创建一个相册界面,使用 `ItemsRepeater` 和 `UniformGridLayout` 显示图片缩略图。尝试添加滚动功能。
> 4. (进阶)实现一个自适应的仪表盘布局:在宽屏时显示 4 列,中等宽度时显示 2 列,窄屏时显示 1 列。使用 `x:Load` 在不同状态下加载不同的布局。
登录后可参与表态
讨论回复
0 条回复还没有人回复,快来发表你的看法吧!