Skip to content

Flutter - 布局简介

在 Flutter 中,布局是通过组合 widget(控件)来确定的。与某些使用单独布局文件(如 XML)的框架不同,你直接在 Dart 代码中使用一套丰富的布局 widget 定义你的 UI(用户界面)结构。理解这些 widget 的工作原理以及如何组合它们,对于创建结构良好且响应式的用户界面至关重要。

核心布局概念:约束下行,尺寸上报,父级定位

Section titled “核心布局概念:约束下行,尺寸上报,父级定位”

Flutter 的布局机制遵循一条简单的规则:

  1. 约束下行 (Constraints go down): 父级 widget 将尺寸约束(最小/最大宽度和高度)传递给其子级。
  2. 尺寸上报 (Sizes go up): 子级 widget 在其约束范围内决定自己的尺寸,并将该尺寸传回给父级。
  3. 父级定位 (Parent sets position): 父级 widget 决定子级在自己内部的位置。

这意味着一个 widget 的尺寸和位置取决于其自身的属性、其父级的约束,以及可能还有其兄弟 widget 的尺寸。

布局 widget 可以根据它们包含多少子级大致分类:

  • 单子 Widget 布局 (Single-Child Layout Widgets): 这些 widget 只有一个 child 属性。
  • 多子 Widget 布局 (Multi-Child Layout Widgets): 这些 widget 有一个 children 属性,接受一个 List<Widget>。

这些 widget 包裹一个子级,以提供特定的布局或样式特性。

  • Container: 一个高度灵活的 widget,用于布局和装饰。它结合了常见的绘画、定位和尺寸设置 widget。

    • 可以设置 width(宽度)、height(高度)、constraints(约束)。
    • 可以应用 padding(内边距,内部空间)和 margin(外边距,外部空间)。
    • 可以通过其 decoration 属性(通常是 BoxDecoration)进行装饰,例如设置 color(颜色)、gradient(渐变)、border(边框)、borderRadius(圆角)、boxShadow(阴影)。
    • 可以使用 alignment(对齐)来对齐其子级。
    • 如果没有提供子级、约束、宽度或高度,它会尝试尽可能大。如果受到约束,它会适应约束。如果它有子级,它会根据子级调整自己的尺寸,除非受到约束。
    • 示例:
  • Padding: 在其子级周围添加空白空间。接受一个 EdgeInsets 值。

  • Center: 将其子级在其内部居中对齐。

  • Align: 根据 Alignment 值(如 Alignment.topRight、Alignment.centerLeft)在其内部对齐其子级。

  • SizedBox: 创建一个指定尺寸的盒子。如果给定一个子级,它会强制子级具有该尺寸。如果未给定子级,它会创建该尺寸的空白空间(在 Row/Column 中用于间隔非常有用)。

  • AspectRatio: 尝试将其子级调整为特定的纵横比。

  • ConstrainedBox: 对其子级施加额外的约束。

  • FittedBox: 根据 BoxFit 参数(如 BoxFit.contain、BoxFit.cover)在其内部缩放和定位其子级。

  • Baseline: 根据父级的基线来定位子级的基线。

  • FractionallySizedBox: 将子级尺寸调整为可用总空间的一部分。

这些 widget 用于排列多个子级。

  • Row: 将其子级水平排列。子级通常需要明确的尺寸设置,或者被包裹在 Expanded 或 Flexible 中以避免溢出。

    • mainAxisAlignment:子级沿主轴(水平)的对齐方式(例如,MainAxisAlignment.start、center、spaceBetween)。
    • crossAxisAlignment:子级沿交叉轴(垂直)的对齐方式(例如,CrossAxisAlignment.start、center、stretch)。
    • 示例:
  • Column: 将其子级垂直排列。属性类似于 Row(mainAxisAlignment 是垂直方向,crossAxisAlignment 是水平方向)。

    • 示例:
  • Stack: 允许子级重叠。子级可以使用 Positioned widget 相对于 Stack 的边界进行定位,或者使用 alignment 属性(默认为左上角)进行对齐。

    • 示例:
  • ListView: 一个可滚动的子级线性排列列表。对于长列表非常高效,因为它只渲染可见的项目。

    • 推荐使用 ListView.builder 来处理动态或大型列表。
    • 示例:
  • GridView: 一个可滚动的二维子级数组。

    • 常见的是 GridView.builder。
    • 需要一个 gridDelegate(例如,SliverGridDelegateWithFixedCrossAxisCount 或 SliverGridDelegateWithMaxCrossAxisExtent)来控制布局。
    • 示例:
  • Expanded & Flexible: 在 Row、Column 或 Flex 中使用,以控制子级如何填充可用空间。

    • Expanded 强制其子级沿主轴填充可用空间(等同于 Flexible 并设置 fit: FlexFit.tight)。它使用 flex 因子与其他 Expanded widget 按比例分配空间。
    • Flexible 允许其子级小于可用空间(fit: FlexFit.loose - 默认值)或强制其填充空间(fit: FlexFit.tight)。
    • 示例:
  • Table: 在表格布局中显示子级。

  • Flow: 用于实现自定义、复杂的布局,其中子级可能会根据可用空间重新定位。

  • Wrap: 水平或垂直排列子级,当空间不足时会换到下一行。

Flutter 布局的强大之处在于组合。你可以组合这些基本的布局 widget 来创建复杂的 UI。例如,一个产品详情屏幕可以使用一个 Column,其中包含一个 Image、一个包含 Row 的 Padding widget 用于标题和价格,以及另一个包含 Text 的 Padding 用于描述。

从简单开始,确定主要的布局方向(水平或垂直),然后相应地嵌套 widget。使用 Flutter Inspector (在 DevTools 中) 等工具来可视化 widget 树并诊断布局问题。

让我们创建一个类似于原始教程示例中的 widget:

import 'package:flutter/material.dart';
class Product {
final String name;
final String description;
final int price;
final String image; // Assume image name corresponds to an asset
Product(this.name, this.description, this.price, this.image);
}
class ProductBox extends StatelessWidget {
final Product item;
const ProductBox({Key? key, required this.item}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(2),
height: 140, // Increased height slightly for better spacing
child: Card(
elevation: 2.0, // Add some shadow
child: Row(
children: <Widget>[
// 确保你已将图片添加到 pubspec.yaml 的 assets 中
// Image.asset("assets/images/${item.image}", width: 100, height: 140, fit: BoxFit.cover),
// 如果未设置图片,使用占位符:
Container(width: 100, height: 140, color: Colors.grey[300], child: Icon(Icons.image, size: 50, color: Colors.grey[600])),
const SizedBox(width: 10), // 图片和文本之间的间距
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 5.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround, // 分布空间
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
item.name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
item.description,
style: Theme.of(context).textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Text(
"Price: \$${item.price}",
style: Theme.of(context).textTheme.titleSmall?.copyWith(color: Colors.green[700]),
),
],
),
),
),
],
),
),
);
}
}
// 在 ListView 中使用示例:
class ProductListScreen extends StatelessWidget {
final List<Product> products = [
Product("iPhone", "iPhone is the stylist phone ever", 1000, "iphone.png"),
Product("Pixel", "Pixel is the most featureful phone ever", 800, "pixel.png"),
// ... more products
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Product Listing")),
body: ListView.builder(
padding: const EdgeInsets.all(8.0), // 在列表周围添加内边距
itemCount: products.length,
itemBuilder: (context, index) {
return ProductBox(item: products[index]);
},
),
);
}
}

此示例使用 Container、Card、Row、Image(或占位符)、SizedBox、Expanded、Padding 和 Column 来有效地构建产品列表项的布局。