flutter的容器布局

MaterialApp

是什么

MaterialApp 是 Flutter 提供的 应用级顶层 Widget,它帮你一次性配置好应用的 主题、路由、导航、本地化 等全局环境,让你只需专注写页面。

如果没有 MaterialApp,你其实还是可以写 Flutter 应用,但很多 MaterialApp 自动帮你做的事情就要自己手动搭,而且步骤会很繁琐。

如果没有 MaterialApp,你得手动写:

  1. Theme(全局主题)
  2. Directionality(文本方向)
  3. Material(Material 风格环境)
  4. Navigator(导航管理)
  5. Localizations(国际化支持)
  6. 调试横幅/标题等额外功能

有Material 风格的程序的构建,当然相对应的是 ios 风格是 CupertinoApp

常用构造参数

参数 作用 示例
title 应用标题(在任务管理器或某些系统 UI 显示) "My App"
home 应用的首页 Widget HomePage()
routes 命名路由表(Map<String, WidgetBuilder> {"/about": (_) => AboutPage()}
theme 主题(Material 风格) ThemeData(primarySwatch: Colors.blue)
darkTheme 暗色模式主题 ThemeData.dark()
themeMode 控制主题模式 ThemeMode.system
debugShowCheckedModeBanner 是否显示右上角 DEBUG 横幅 false
locale / supportedLocales 多语言支持 Locale('en')
onGenerateRoute 动态路由生成逻辑 自定义函数
const MaterialApp({
  Key key,
  // 导航键 , key的作用提高复用性能
  this.navigatorKey,
  // 主页
  this.home,
  // 路由
  this.routes = const <String, WidgetBuilder>{},
  // 初始命名路由
  this.initialRoute,
  // 路由构造
  this.onGenerateRoute,
  // 未知路由
  this.onUnknownRoute,
  // 导航观察器
  this.navigatorObservers = const <NavigatorObserver>[],
  // 建造者
  this.builder,
  // APP 标题
  this.title = '',
  // 生成标题
  this.onGenerateTitle,
  // APP 颜色
  this.color,
  // 样式定义
  this.theme,
  // 主机暗色模式
  this.darkTheme,
  // 样式模式
  this.themeMode = ThemeMode.system,
  // 多语言 本地化
  this.locale,
  // 多语言代理
  this.localizationsDelegates,
  // 多语言回调
  this.localeListResolutionCallback,
  this.localeResolutionCallback,
  // 支持的多国语言
  this.supportedLocales = const <Locale>[Locale('en', 'US')],
  // 调试显示材质网格
  this.debugShowMaterialGrid = false,
  // 显示性能叠加
  this.showPerformanceOverlay = false,
  // 检查缓存图片的情况
  this.checkerboardRasterCacheImages = false,
  // 检查不必要的setlayer
  this.checkerboardOffscreenLayers = false,
  // 显示语义调试器
  this.showSemanticsDebugger = false,
  // 显示debug标记 右上角
  this.debugShowCheckedModeBanner = true,
})

简单示例

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // APP 标题
      // ios 没有用、 android 进程名称 、 web 标题tab栏名称
      title: 'Material App',

      // APP 颜色
      color: Colors.green,

      // 样式
      theme: ThemeData(
        primarySwatch: Colors.yellow,
      ),

      // 主机暗色模式
      darkTheme: ThemeData(
        primarySwatch: Colors.red,
      ),

      // 显示debug标记 右上角
      // debugShowCheckedModeBanner: false,

      // 调试显示材质网格
      // debugShowMaterialGrid: true,

      // 显示性能叠加
      // showPerformanceOverlay: true,

      // 检查缓存图片的情况
      // checkerboardRasterCacheImages: true,

      // 检查不必要的setlayer
      // checkerboardOffscreenLayers: true,

      // 显示语义调试器
      // showSemanticsDebugger: true,

      // 首页
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Material App'),
        ),
        body: Center(
          child: Column(
            children: const [
              Text("data"),
              FlutterLogo(
                size: 100,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Scaffold

脚手架

定义:一种用来快速生成项目、模块或页面基本骨架的工具或组件,省去重复写模板代码的过程。它可以是:

  • 命令行工具(例如:vue create my-app 会生成一个 Vue 项目骨架)
  • 代码框架类(例如:Flutter 的 Scaffold 帮你生成页面基础布局)
  • 自动化生成器(例如:Django 的 startapp 会帮你生成 MVC 目录结构)

为什么叫做脚手架:

  • 在建筑里,脚手架本身不是房子的一部分,但它提供了施工的支撑,让工人能安全高效地完成工作。
  • 在软件里,脚手架代码本身可能不是业务核心,但它提供了开发的基础结构,让开发者能更快地进入业务逻辑编写。

常见软件脚手架例子:

场景 脚手架示例 作用
前端 create-react-app / vue-cli 生成项目目录、配置文件、入口文件
后端 django-admin startproject 创建 MVC 结构、配置数据库连接
Flutter Scaffold 提供 AppBar、body、drawer 等页面布局骨架
Node.js express-generator 自动生成路由、视图、静态文件结构

在 Flutter 里,Scaffold 叫做“页面布局脚手架”,意思是它帮你:

  • 先搭好一个页面的标准结构(AppBar、body、Drawer、BottomNavigationBar、FAB…)
  • 你只需要往这些位置放内容(Widget)就行,不必自己手写布局来摆放顶部、底部等

Scaffold对应 ios 的是 CupertinoPageScaffold

Scaffold 的主要作用

提供 Material 风格的基础布局结构

内置多个常用布局槽位(AppBar、Body、Drawer、BottomNavigationBar…)

统一管理 Material 特性(如 SnackBarBottomSheet 的显示)

Scaffold 常用参数

参数 作用 示例
appBar 顶部应用栏 AppBar(title: Text('标题'))
body 主体内容区域 Center(child: Text('内容'))
drawer 左侧抽屉菜单 Drawer(child: ListView(...))
endDrawer 右侧抽屉菜单 Drawer(...)
bottomNavigationBar 底部导航栏 BottomNavigationBar(items: [...])
floatingActionButton 悬浮按钮 FloatingActionButton(...)
backgroundColor 页面背景色 Colors.white
bottomSheet 底部固定面板 BottomSheet(...)
persistentFooterButtons 固定在底部的按钮组 [TextButton(...), TextButton(...)]
const Scaffold({
    Key key,
    // 菜单栏
    this.appBar,
    // 中间主体内容部分
    this.body,
    // 悬浮按钮
    this.floatingActionButton,
    // 悬浮按钮位置
    this.floatingActionButtonLocation,
    // 悬浮按钮动画
    this.floatingActionButtonAnimator,
    // 固定在下方显示的按钮
    this.persistentFooterButtons,
    // 左侧 侧滑抽屉菜单
    this.drawer,
    // 右侧 侧滑抽屉菜单
    this.endDrawer,
    // 底部菜单
    this.bottomNavigationBar,
    // 底部拉出菜单
    this.bottomSheet,
    // 背景色
    this.backgroundColor,
    // 自动适应底部padding
    this.resizeToAvoidBottomPadding,
    // 重新计算body布局空间大小,避免被遮挡
    this.resizeToAvoidBottomInset,
    // 是否显示到底部,默认为true将显示到顶部状态栏
    this.primary = true,
  	// 用来控制侧边抽屉(Drawer)滑动手势的响应方式。
    this.drawerDragStartBehavior = DragStartBehavior.down,
  })

drawerDragStartBehavior

drawerDragStartBehaviorScaffold 的一个可选参数,用来控制侧边抽屉(Drawer)滑动手势响应方式。它的类型是 DragStartBehavior 枚举,取值有:

  • DragStartBehavior.start
  • DragStartBehavior.down

在 Flutter 中,Drawer 可以通过手势(从屏幕边缘滑动)拉出来。手势事件的触发有两种时机:

  • DragStartBehavior.start(默认值):只有当手指真正滑动(有位移)时才触发拖动逻辑。也就是说,触摸屏幕不动,不会立刻触发拖动。
  • DragStartBehavior.down: 只要手指按下(down)就立即开始拖动逻辑,不等滑动位移。适合需要快速响应拖动的场景。
模式 行为
start 按下屏幕后必须有滑动位移,拖动才开始(更自然,防止误触)
down 按下的瞬间就进入拖动状态(响应更快,但可能会误触)

大多数情况下用默认的 start 就好,避免用户只是点一下屏幕边缘时就意外拉出抽屉。

代码示例

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

// 下面这段注释了的代码是一个 Cupertino 风格的页面示例,
// class CupertinoPage extends StatelessWidget {
//   const CupertinoPage({Key? key}) : super(key: key);

//   @override
//   Widget build(BuildContext context) {
//     return const CupertinoPageScaffold(
//       navigationBar: CupertinoNavigationBar(
//         middle: Text('我是标题'),
//       ),
//       child: Center(
//         child: Text('我是内容'),
//       ),
//     );
//   }
// }

void main() {
  runApp(const ScaffoldPage());
}

class ScaffoldPage extends StatelessWidget {
  const ScaffoldPage({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        // 菜单栏
        appBar: AppBar(title: const Text('菜单栏')),

        // 悬浮按钮
        floatingActionButton: FloatingActionButton(
          onPressed: () {},
          child: const Icon(Icons.add_photo_alternate),
        ),

        // 悬浮按钮位置
        floatingActionButtonLocation: FloatingActionButtonLocation.centerTop,

        // 固定在下方显示的按钮
        persistentFooterButtons: const [
          Text('固定在下方显示的按钮1'),
          Text('固定在下方显示的按钮2'),
        ],

        // 压缩顶部菜单空间
        primary: true,

        // 左侧 侧滑抽屉菜单
        drawer: const Drawer(child: Text('data')),

        // 右侧 侧滑抽屉菜单
        endDrawer: const Drawer(child: Text('data')),

        // 检测手势行为方式,与drawer配合使用 down 方式有卡顿,可以 start 方式
        drawerDragStartBehavior: DragStartBehavior.start,

        // 底部导航栏
        bottomNavigationBar: const Text('底部导航栏'),

        // 底部拉出菜单
        bottomSheet: const Text('底部拉出菜单'),

        // 背景色
        backgroundColor: Colors.amberAccent,

        // 自动适应底部padding
        resizeToAvoidBottomInset: true,

        // 正文:
        // Builder 是一个特殊的 Widget,用来创建一个新的 BuildContext。
        // 这么做的原因:Scaffold.of(context) 在 Flutter 里是用来获取离当前 context 最近的 ScaffoldState 实例的。
        // 换句话说:找到当前 Widget 树里最靠近它的 Scaffold,然后你就可以用这个 Scaffold 的方法(比如 openDrawer()、showSnackBar())来操作页面。
        // 如果直接用外部的 context,可能找不到最近的 Scaffold,会报错。
        // 所以这里用 Builder 创建了一个位于 Scaffold 内部的新 context,让 Scaffold.of(context) 能找到 Scaffold。
        body: Builder(
          builder: (BuildContext context) {
            return Center(
              child: ElevatedButton(
                onPressed: () {
                  // 脚手架管理
                  // 从当前 context 向上查找最近的 Scaffold,获取它的状态对象 ScaffoldState。
                  // 调用 openDrawer() 方法,手动打开 Scaffold 的 drawer(左侧抽屉菜单)。
                  // 这是一个页面结构操作。
                  Scaffold.of(context).openDrawer();

                  // 应用消息管理
                  // ScaffoldMessenger 是 Flutter 用来管理 SnackBar 的组件(比旧的 Scaffold.of(context).showSnackBar() 更灵活)。
                  // 它会从当前 context 找到最近的 ScaffoldMessenger,然后显示一个 SnackBar
                  // SnackBar 是一个底部弹出的提示条,这里显示 "Hello!",会在 2 秒后自动消失。
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text('Hello!'),
                      duration: Duration(seconds: 2),
                    ),
                  );
                },
                child: const Text('应用消息管理'),
              ),
            );
          },
        ),
      ),
    );
  }
}

线性布局

在 Flutter 中,线性布局通常指用 RowColumn 这两个 Widget 来实现的布局方式。它们都是 线性排列的容器,可以把子 Widget 按 水平或垂直方向依次排列。

Widget 作用 方向
Row 水平排列子 Widget 左到右(LTR)或右到左(RTL)
Column 垂直排列子 Widget 上到下

定义一个线性布局

以下是Row的源码,Column的源码也类似

Row({
    Key key,
    
    // * 子元素集合
    List<Widget> children = const <Widget>[],
  
    // 主轴方向上的对齐方式(Row的主轴是横向轴)
    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
    // 在主轴方向(Row的主轴是横向轴)占有空间的值,默认是max
    MainAxisSize mainAxisSize = MainAxisSize.max,
    // 在交叉轴方向(Row是纵向轴)的对齐方式,Row的高度等于子元素中最高的子元素高度
    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
    
        // 水平方向子元素的排列方向:从左到右排列还是反向
    TextDirection textDirection,
    // 表示纵轴(垂直)的对齐排列方向,默认是VerticalDirection.down,表示从上到下。这个参数一般用于Column组件里
    VerticalDirection verticalDirection = VerticalDirection.down,
    // 字符对齐基线方式
    TextBaseline textBaseline,

  })

常用属性

主轴(Main Axis)

mainAxisAlignment:沿 主轴(Row → 水平,Column → 垂直)对齐方式

常用值:

  • start:靠起点对齐
  • end:靠终点对齐
  • center:居中
  • spaceBetween:两端对齐,间距平均分配
  • spaceAround:间距平均分配,首尾留一半间距
  • spaceEvenly:间距平均分配,首尾和中间一致
enum MainAxisAlignment {
  // 按照主轴起点对齐,例如:按照靠近最左侧子元素对齐
  start,

  // 将子元素放置在主轴的末尾,按照末尾对齐
  end,

  // 子元素放置在主轴中心对齐
  center,

  // 将主轴方向上的空白区域均分,使得子元素之间的空白区域相等,首尾子元素都靠近首尾,没有间隙。有点类似于两端对齐
  spaceBetween,

  // 将主轴方向上的空白区域均分,使得子元素之间的空白区域相等,但是首尾子元素的空白区域为1/2
  spaceAround,

  // 将主轴方向上的空白区域均分,使得子元素之间的空白区域相等,包括首尾子元素
  spaceEvenly,
}

交叉轴(Cross Axis)

crossAxisAlignment:沿 交叉轴(Row → 垂直,Column → 水平)对齐方式

常用值:

  • start / end / center / stretch(填满交叉轴)
enum CrossAxisAlignment {
  // 子元素在交叉轴上起点处展示
  start,

  // 子元素在交叉轴上末尾处展示
  end,

  // 子元素在交叉轴上居中展示
  center,

  // 让子元素填满交叉轴方向
  stretch,

  // 在交叉轴方向,使得子元素按照baseline对齐
  baseline,
}

mainAxisSize

  • mainAxisSize:控制主轴占用空间大小
  • MainAxisSize.max → 占满父容器
  • MainAxisSize.min → 只包裹子 Widget 所需大小
enum MainAxisSize {
  // 根据传入的布局约束条件,最大化主轴方向占用可用空间,也就是尽可能充满可用宽度
  max,

  // 与max相反,是最小化占用主轴方向的可用空间
  min,
}

代码示例

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'Flutter Demo', home: const MyHomePage());
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        color: Colors.amber,
        child: Column(
          // MainAxisAlignment.center : 纵向居中
          mainAxisAlignment: MainAxisAlignment.center,
          // CrossAxisAlignment.stretch : 横向拉伸
          crossAxisAlignment: CrossAxisAlignment.stretch,
          // MainAxisSize.min : 纵向压缩
          mainAxisSize: MainAxisSize.min,
          children: const [
            FlutterLogo(size: 24),
            FlutterLogo(size: 48),
            FlutterLogo(size: 128),
            FlutterLogo(size: 200),
          ],
        ),
      ),
    );
  }
}

Box Model 盒模型

Flutter 的盒模型组成

一个 Widget(尤其是 Container)通常可以看作一个 矩形盒子,它由 四层 组成,从里到外:

Child(子 Widget):实际显示的内容

Padding(内边距):Child的 Widget 与边框(Border)的间距

Border(边框):盒子外层的边框线,可以设置边框厚度

Margin(外边距):盒子与外部其他 Widget 的间距

常见属于盒模型的 Widget

Widget 类型 盒模型元素支持情况 说明
Container child + padding + border + margin 最典型的盒子模型 Widget,可以控制内边距、外边距、边框、背景色等
Padding padding 专门用来增加内边距
Align / Center child + alignment 可以对 child 进行对齐,但不直接提供 margin/padding
DecoratedBox decoration(可含边框/背景色) 提供边框和背景装饰,但没有内边距或外边距,需要配合 Padding/Container
SizedBox width/height 用于控制子 Widget 尺寸,相当于“约束盒子”
Card margin + child + decoration Material 风格卡片,有默认外边距、圆角、阴影
ConstrainedBox constraints 用来给 child 设置最小/最大尺寸

不是典型盒子模型的 Widget

TextIconImage渲染内容 Widget

  • 它们本身是内容,不带 padding/margin/border
  • 需要用 ContainerPadding 包裹才能形成完整盒子

RowColumnStack布局容器

  • 它们是父容器,用来排列子 Widget,本身不一定有 margin/border/padding
  • 可以通过包裹 Container 来形成盒子效果

代码示例

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'Flutter Demo', home: const BoxPage());
  }
}

class BoxPage extends StatelessWidget {
  const BoxPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.blue,
      body: Center(
        child: Container(
          // color: Colors.amber,

          // Margin(外边距)
          margin: const EdgeInsets.all(50),

          // Padding(内边距)
          padding: const EdgeInsets.all(20),

          // 装饰样式
          decoration: BoxDecoration(
            // 背景色
            color: Colors.amber,

            // 边框
            border: Border.all(color: Colors.red, width: 10),
          ),

          // Content(内容)
          child: Container(width: 200, height: 400, color: Colors.green),
        ),
      ),
    );
  }
}

容器Container

在 Flutter 中,Container 是最常用、最基础的布局 Widget之一,它本质上是一个 可绘制的矩形盒子,可以用来控制 尺寸、边距、内边距、边框、背景色、形状和对齐方式

Container 的定义:

Container({
    Key key,
    // 容器子Widget对齐方式
    this.alignment,
    // 容器内部padding
    this.padding,
    // 背景色
    Color color,
    // 背景装饰: 可设置背景色、边框、圆角、渐变、阴影等
    Decoration decoration,
    // 前景装饰
    this.foregroundDecoration,
    // 容器的宽度
    double width,
    // 容器的高度
    double height,
    // 容器大小的限制条件
    BoxConstraints constraints,
    // 容器外部margin
    this.margin,
    // 变换,如旋转
    this.transform,
    // 容器内子Widget
    this.child,
  })

BoxDecoration 装饰

在 Flutter 中,BoxDecoration 是一个用来 美化容器(Container)或其他可绘制盒子 Widget 的类,它属于 装饰(decoration) 的概念。

BoxDecoration = 容器的“外观样式”,控制背景、边框、圆角、阴影、渐变等视觉效果,而不是布局或尺寸。

const BoxDecoration({
  // 背景色
  this.color,
  // 背景图片
  this.image,
  // 边框样式
  this.border,
  // 边框圆角
  this.borderRadius,
  // 阴影
  this.boxShadow,
  // 渐变
  this.gradient,
  // 背景混合模式
  this.backgroundBlendMode,
  // 形状
  this.shape = BoxShape.rectangle,
})

示例

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'Flutter Demo', home: const MyPage());
  }
}

const img1 = "https://linresource.uk/img/13399635147767705.jpg";

class MyPage extends StatelessWidget {
  const MyPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        // 约束父容器
        constraints: const BoxConstraints.expand(height: 400.0),

        // 外边距
        // EdgeInsets.only(top: 40): 仅设置上边距为 40.0 像素
        margin: const EdgeInsets.only(top: 40),

        // 内边距
        // EdgeInsets.all(30.0): 所有方向的内边距都设置为 30.0 像素
        padding: EdgeInsets.all(30.0),

        // 背景色
        // color: Colors.teal.shade700,

        // 子Widget: Alignment.centerLeft 表示左对齐且居中
        // Alignment.center 表示居中对齐
        alignment: Alignment.center,

        // 背景装饰
        decoration: BoxDecoration(
          // 背景色
          color: Colors.blueAccent,
          // 圆角
          borderRadius: BorderRadius.all(Radius.circular(20.0)),
          // 渐变
          // gradient: RadialGradient(
          //   colors: [Colors.red, Colors.orange],
          //   center: Alignment.topLeft,
          //   radius: .98,
          // ),
          // 阴影
          boxShadow: [
            BoxShadow(
              blurRadius: 10,
              offset: Offset(0, 50),
              color: Colors.blue,
            ),
          ],
          // 背景图
          image: DecorationImage(image: NetworkImage(img1), fit: BoxFit.cover),
          // 背景混合模式
          backgroundBlendMode: BlendMode.color,
          // 形状
          // shape: BoxShape.circle,
        ),

        // 前景装饰
        foregroundDecoration: BoxDecoration(
          // 前景色:withOpacity 已经过时
          // color: Colors.amber.withOpacity(0.5),
          // color: Colors.amber.withValues(alpha: 0.5),
          color: Colors.amber.withAlpha(128),
        ),

        // Container旋转
        transform: Matrix4.rotationZ(0.1),

        // 子Widget元素
        child: Text(
          'Hello World',
          style: Theme.of(
            context,
          ).textTheme.headlineMedium?.copyWith(color: Colors.white),
        ),
      ),
    );
  }
}

设置内外边距的方法

Container(
  padding: EdgeInsets.all(16), // 所有方向 16 像素
  child: Text('内边距示例'),
)
方法 说明
EdgeInsets.all(double value) 四个方向(上、下、左、右)相同值
EdgeInsets.symmetric({double vertical, double horizontal}) 垂直方向和水平方向分别设置
EdgeInsets.only({left, top, right, bottom}) 单独设置每个方向
EdgeInsets.fromLTRB(left, top, right, bottom) 另一种单独设置四个方向的方法

Colors组件的两个方法

withAlpha() 设置透明度

withAlpha(int a)0~255 的值 设置透明度:

Color color = Colors.amber.withAlpha(128); // 半透明

0 → 完全透明

255 → 完全不透明

withValues()(Flutter 3.13+ 新 API)

Color withValues({
  double? alpha,
  double? red,
  double? green,
  double? blue,
  ColorSpace? colorSpace,
})
参数 类型 说明
alpha double? 透明度,0.0 = 完全透明,1.0 = 不透明
red double? 红色分量,0.0 ~ 1.0
green double? 绿色分量,0.0 ~ 1.0
blue double? 蓝色分量,0.0 ~ 1.0
colorSpace ColorSpace? 颜色空间(可选,默认 sRGB)

弹性布局 Flex

什么是弹性布局

在软件开发和 UI 布局中,**弹性布局(Flexible Layout / Flex Layout)**指的是:子元素可以根据可用空间动态伸缩,占用父容器剩余空间或按比例分配空间,而不是固定尺寸。

换句话说,弹性布局的核心就是 空间可以“弹性”分配,不必每个子元素都写死宽高。

在 Flutter 中,弹性布局(Flex) 是一种 按比例分配空间的线性布局,可以理解为 “增强版的 Row/Column”,它允许子 Widget 按比例(flex 值)来分配父容器的剩余空间。

RowColumn 本质上就是 Flex 的封装,Flex 的方向不同而已。

Flex 的定义

和前面的RowColumn 类似。

Flex({
  Key? key,
  required Axis direction,      // 主轴方向: Axis.horizontal / Axis.vertical
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  MainAxisSize mainAxisSize = MainAxisSize.max,
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  TextDirection? textDirection,
  VerticalDirection verticalDirection = VerticalDirection.down,
  TextBaseline? textBaseline,
  List<Widget> children = const <Widget>[],
})
属性 说明
direction 主轴方向:水平(Row)或垂直(Column)
mainAxisAlignment 主轴对齐方式
crossAxisAlignment 交叉轴对齐方式
children 子 Widget 列表
mainAxisSize 主轴占用空间大小(min/max)

Expanded

Expanded 只能放在 Flex、Column、Row 中使用

Expanded的作用:

  • 强制子 Widget 占满剩余空间
  • flex 决定占剩余空间的比例

强制子 Widget 占满剩余空间

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'Flutter Demo', home: const FlexPage());
  }
}

class FlexPage extends StatelessWidget {
  const FlexPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          // 尽管设置了宽度,但由于使用了 Expanded,仍然会占据剩余空间
          Expanded(child: Container(color: Colors.amber, width: 100)),
          Container(color: Colors.blue, width: 100, height: 100),
        ],
      ),
    );
  }
}
class FlexPage extends StatelessWidget {
  const FlexPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          // 尽管设置了宽度,但由于使用了 Expanded,仍然会占据剩余空间
          Expanded(child: Container(color: Colors.amber, width: 10)),
          Expanded(child: Container(color: Colors.blue, width: 600)),
        ],
      ),
    );
  }
}

Expanded修饰的两个容器的width被平均分别,尽管两个容器都设置了width的大小

flex 决定占剩余空间的比例

class FlexPage extends StatelessWidget {
  const FlexPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          // 尽管设置了宽度,但由于使用了 Expanded,仍然会占据剩余空间
          Expanded(flex: 2, child: Container(color: Colors.amber, width: 10)),
          Expanded(flex: 1, child: Container(color: Colors.blue, width: 600)),
        ],
      ),
    );
  }
}

水平方向空间被 2:1 比例分配给黄色和蓝色容器

Flexible

子 Widget 可按比例占用空间,但允许自身缩小或包裹内容

fit 可选择 FlexFit.tight(填满)或 FlexFit.loose(包裹内容)

没有Flexible按照设置的widthheight布局

class FlexPage extends StatelessWidget {
  const FlexPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        color: Colors.grey,
        height: 500,
        child: Column(
          children: [
            Container(color: Colors.amber, height: 100),
            Container(color: Colors.blue, height: 100),
          ],
        ),
      ),
    );
  }
}

使用Flexible:


class FlexPage extends StatelessWidget {
  const FlexPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        color: Colors.grey,
        height: 500,
        child: Column(
          children: [
            Flexible(child: Container(color: Colors.amber)),
            Flexible(child: Container(color: Colors.blue)),
          ],
        ),
      ),
    );
  }

使用Flexible之后,两个容器共同分享了剩余的300的空间,平均每一个是250的高度。

另外,这里我的两个我都没有设置height,这是因为,如果我Flexible 不会自动忽略子组件的固定高度height: 100,而是让子组件 在剩余空间内,按需要伸缩。如果给给 Container 明确写了 height: 100,所以它们不会去填满剩余的 300 高度。所以我在这里去掉了height设置。

fit配置

fit 在 Flutter 的弹性布局里其实就是控制 “给子组件的空间是紧的还是松的”

FlexFit.loose(默认值):

  • 父组件把空间分配好后,你可以决定自己的尺寸(如果设置了固定高度,就用固定高度;没设置就会尽量扩展)

FlexFit.tight

  • 父组件把分配到的剩余空间按比例分好,然后强制子组件填满这块地盘(如果设置了高度和宽度也是无效的)。
  • Expanded 就是 Flexible(flex: 1, fit: FlexFit.tight) 的简写。
class FlexPage extends StatelessWidget {
  const FlexPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        color: Colors.grey,
        height: 500,
        child: Column(
          children: [
            Flexible(
              fit: FlexFit.tight,
              child: Container(color: Colors.amber, height: 100),
            ),
            Flexible(
              fit: FlexFit.tight,
              child: Container(color: Colors.blue, height: 100),
            ),
          ],
        ),
      ),
    );
  }
}

使用fix分配空间

class FlexPage extends StatelessWidget {
  const FlexPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        color: Colors.grey,
        height: 500,
        child: Column(
          children: [
            // Container(color: Colors.amber),
            // Container(color: Colors.blue),
            // Container(color: Colors.amber, height: 100),
            // Container(color: Colors.blue, height: 100),
            // Flexible(child: Container(color: Colors.amber, height: 100)),
            // Flexible(child: Container(color: Colors.blue, height: 100)),
            Flexible(
              flex: 1,
              fit: FlexFit.tight,
              child: Container(color: Colors.amber, height: 100),
            ),
            Flexible(
              flex: 2, 
              fit: FlexFit.tight,
              child: Container(color: Colors.blue, height: 100),
            ),
          ],
        ),
      ),
    );
  }
}

500的空间按照1:2的比例分配

Spacer

Spacer 其实就是 一个专门用来“占空”并推开其它组件的工具,它本质上就是 Expanded 的一个简化版。

在 Flutter 的 RowColumn 中,Spacer 会:

  • 占据可用的剩余空间
  • 不能设置宽度/高度
  • 可通过 flex 参数控制占用比例

内部实现等同于:

Expanded(
  flex: x, // 默认 1
  child: SizedBox.shrink(), // 一个啥也没有的空盒子
)

Spacer 主要用来在弹性布局中调整组件之间的间距,而不需要手动加 Expanded + Container

class FlexPage extends StatelessWidget {
  const FlexPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        color: Colors.grey,
        height: 500,
        child: Column(
          children: [
            Flexible(
              flex: 3,
              fit: FlexFit.tight,
              child: Container(color: Colors.amber, height: 100),
            ),
            const Spacer(), // 默认flex=1
            Flexible(
              flex: 3,
              fit: FlexFit.tight,
              child: Container(color: Colors.blue, height: 100),
            ),
            const Spacer(flex: 2), // flex=2
          ],
        ),
      ),
    );
  }
}

层叠布局 Stack

基本作用

Stack 会把多个子组件 叠放在一起(Z 轴方向的布局),和 Row / Column 不同的是:

  • Row / Column 是沿 X / Y 轴排列
  • Stack 是沿 Z 轴 叠放

以把 Stack 想象成 Photoshop 里的图层系统,后添加的元素会叠在前面的上面。

在 Flutter 的 Stack 中,默认的排列(叠放)顺序就是按照 children 列表里的代码顺序来决定的:

  • 前面写的子组件(列表中靠前的)会先绘制,所以它会在下面
  • 后面写的子组件会后绘制,所以它会盖在上面

Stack的定义

Stack({

    Key key,
    
    // 对齐方式,默认是左上角(topStart)
    this.alignment = AlignmentDirectional.topStart,
    
    // 对齐方向
    this.textDirection,
    
    // 定义如何设置无定位子元素尺寸,默认为loose
    this.fit = StackFit.loose,
  
  // 对超出 Stack 显示空间的部分如何剪裁
  this.clipBehavior = Clip.hardEdge,
    
    // 子元素
    List<Widget> children = const <Widget>[],

})

基本语法

class StackPage extends StatelessWidget {
  const StackPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        // 居中对齐
        alignment: Alignment.center,
        // 子元素层叠放
        children: [
          // 三个色块
          Container(width: 100, height: 100, color: Colors.green),
          Container(width: 300, height: 300, color: Colors.amber),
          Container(width: 200, height: 200, color: Colors.blue),
        ],
      ),
    );
  }
}

由于绿色色块更靠前,所有先绘制,又由于绿色色块小,会被红色色块盖住,所有不会显示。

Stack的大小默认由最大子元素(300×300 的那个容器)决定。

Positioned绝对定位

class StackPage extends StatelessWidget {
  const StackPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        // 居中对齐
        alignment: Alignment.center,
        // 子元素溢出, none 不裁剪
        clipBehavior: Clip.none,
        // 子元素层叠放
        children: [
          // 三个色块
          Container(width: 100, height: 100, color: Colors.green),
          Container(width: 300, height: 300, color: Colors.amber),
          Container(width: 200, height: 200, color: Colors.blue),

          // 绝对定位
          const Positioned(left: 0, bottom: -50, child: FlutterLogo(size: 100)),
        ],
      ),
    );
  }
}

clipBehavior:决定超出 Stack 范围的内容是否裁剪:

  • Clip.none:不裁剪
  • Clip.hardEdge / Clip.antiAlias:裁剪掉溢出的部分

Positioned 是用来给子组件绝对定位的,可以用 left/right/top/bottom 任意组合控制位置。

const Positioned({

    Key key,
    
    this.left, // 上下左右位置
    this.top,
    this.right,
    this.bottom,
    
    this.width, // 宽高
    this.height,
    
    @required Widget child,
  
})

流式布局 Wrap

什么是流式布局

  • Row / Column 的子组件超出主轴空间时会溢出报错(出现黄黑相间的“溢出警告条”)。
  • Wrap 会自动换行(或换列),像文字一样流动,所以叫“流式布局”。
  • 常用于标签列表、照片墙、按钮组等场景。

Wrap的定义

Wrap({
  // 主轴方向: 水平(Axis.horizontal)或垂直(Axis.vertical) 默认水平方向
  this.direction = Axis.horizontal,
  //主轴方向的对齐方式(每一行内部怎么排)
  this.alignment = WrapAlignment.start,
  // 主同一行(列)中,子组件之间的间距
  this.spacing = 0.0,
  // 纵向(换行方向)的对齐方式
  this.runAlignment = WrapAlignment.start,
  // 行与行之间(或列与列之间)的间距
  this.runSpacing = 0.0,
  // 交叉轴方向的对齐方式
  this.crossAxisAlignment = WrapCrossAlignment.start,
  // textDirection / verticalDirection 控制排列方向(从左到右/从右到左,从上到下/从下到上)
  this.textDirection,
  this.verticalDirection = VerticalDirection.down,
  List<Widget> children = const <Widget>[],
})
参数 作用
direction 主轴方向,水平(Axis.horizontal)或垂直(Axis.vertical
spacing 同一行(列)中,子组件之间的间距
runSpacing 行与行之间(或列与列之间)的间距
alignment 主轴方向的对齐方式(每一行内部怎么排)
runAlignment 纵向(换行方向)的对齐方式
crossAxisAlignment 交叉轴方向的对齐方式
textDirection / verticalDirection 控制排列方向(从左到右/从右到左,从上到下/从下到上)

代码示例


class WrapPage extends StatelessWidget {
  const WrapPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SizedBox(
        height: 600,
        child: Wrap(
          // 主轴方向子widget的间距
          spacing: 10,
          // 交叉轴方向的间距
          runSpacing: 30,
          // 主轴方向的对齐方式
          alignment: WrapAlignment.end,
          runAlignment: WrapAlignment.center,
          crossAxisAlignment: WrapCrossAlignment.center,
          children: [
            Container(width: 70, height: 100, color: Colors.green),
            Container(width: 10, height: 100, color: Colors.blue),
            Container(width: 70, height: 100, color: Colors.red),
            Container(width: 70, height: 80, color: Colors.yellow),
            Container(width: 70, height: 80, color: Colors.orange),
            Container(width: 10, height: 80, color: Colors.purple),
            Container(width: 70, height: 80, color: Colors.brown),
            Container(width: 70, height: 100, color: Colors.grey),
            Container(width: 170, height: 100, color: Colors.black),
            Container(width: 70, height: 10, color: Colors.pink),
            Container(width: 20, height: 100, color: Colors.cyan),
            Container(width: 70, height: 10, color: Colors.lime),
          ],
        ),
      ),
    );
  }
}

runAlignmentcrossAxisAlignment 的区别

crossAxisAlignment

  • 控制 每一行内 元素在交叉轴上的对齐方式。

  • 类似于 RowcrossAxisAlignment

  • 生效范围:单行内部的元素。

  • 例子(Axis.horizontal):所有元素都贴在行顶部对齐。在一行内

    crossAxisAlignment: start
    ┌─────────────── 容器 ──────────────┐
    [█   ]
    [██  ]
    [███ ]
    

runAlignment

  • 控制 整行之间 在交叉轴方向的对齐方式。

  • 影响的是多行之间的分布,不是单行内的对齐。

  • 类似于 ColumnmainAxisAlignment(因为换行后每一行就像一个 “子Column”)。

  • 例子(Axis.horizontal):交叉轴方向(垂直)靠下

    +--------------------+
                         ← 空白
    [AAA AAA AAA]        ← 第一行
    [BBB BBB BBB]        ← 第二行
    [CCC CCC]            ← 第三行
    +--------------------+
    
  • runAlignment 成立的前提是,Wrap的大小,要大于 Wrap内所有元素的和

SizedBox

SizedBox 主要是创建一个固定尺寸的容器

SizedBox(width: 300, height: 300) 表示它的宽度固定为 300,高度固定为 300。

区别于 Container

  • SizedBox 专注于固定宽高和占位,本身没有背景色、边框等修饰属性。
  • Container 功能更多(可以设置背景色、边框、内外边距等)。

SizedBox 也可以只指定宽度或高度,用来占据特定的空间

SizedBox(height: 20); // 相当于竖直方向加一个间距
SizedBox(width: 10);  // 横向间距

特殊构造函数

  • SizedBox.shrink():尺寸为 0×0,通常用来“占位但不显示”。
  • SizedBox.expand():尽可能填满父组件给的约束空间。

约束传递(constraints passing)

Flutter 布局的三步规则:

  1. 父传约束(constraints)给子
  2. 子在约束范围内确定自己的大小(size)
  3. 父根据子大小和对齐方式来摆放(position)

SizedBox(height: 600) 的行为

  • SizedBox(height: 600) 会生成一个 BoxConstraints,相当于:

    minHeight = 600
    maxHeight = 600
    

    也就是固定高度

  • 这个约束会被直接传递给它的子组件,比如:

    SizedBox(height: 600,
      child: Wrap(...),
    );
    

    Wrap 收到的约束就是 “必须正好 600 高度”
    Wrap 再把这个约束传给它的孩子们(但会根据自己的布局规则调整)。

Container 的行为

  • 如果你给 Container 设置了 height / width,它的表现和 SizedBox 是一样的:直接在约束中锁定对应尺寸。
  • 如果没设置尺寸,Container 会尽可能去匹配父给的约束,可能会变成 Expanded 一样的拉伸行为,或是和内容一样大(取决于父的约束)。

wrap的默认大小

Wrap 在 Flutter 中的默认大小有两个要点:

  1. 主轴方向(direction)默认是水平的
    • 主轴方向的大小会自适应内容宽度,直到撑满父容器的约束为止。
    • 如果父容器给了无限空间(比如 SingleChildScrollView 横向包裹),那它会根据子元素的总宽度来定。
  2. 交叉轴方向(垂直方向,如果 direction 是水平)默认也是自适应内容
    • 它会计算出所有行的总高度(每行的高度 + runSpacing),正好包裹所有行

总结:

  • 如果没有父类的约束,那么Wrap的大小就是子元素的总和
  • 如果有父类的约束,那么Wrap的大小就是父约束指定的大小

对齐定位 Align

Align 是什么

Align 是一个用于 对子组件进行对齐和定位 的布局组件。它的作用是:

  • 根据 alignment 参数,将 子组件 在自身范围内按指定位置摆放。
  • 可以配合 widthFactorheightFactor 决定 Align 自己的大小。

Align的定义

Align({
  Key key,
  
  // 需要一个AlignmentGeometry类型的值
  // AlignmentGeometry 是一个抽象类,
  // 它有两个常用的子类:Alignment和 FractionalOffset
  this.alignment = Alignment.center,
  
  // 两个缩放因子
  // 会分别乘以子元素的宽、高,最终的结果就是 Align 组件的宽高
  this.widthFactor,
  this.heightFactor,
  Widget child,
})

alignment:控制子组件在 Align 内部的位置,常用值如下

含义
Alignment.center 居中
Alignment.topLeft 左上角
Alignment.topRight 右上角
Alignment.bottomLeft 左下角
Alignment.bottomRight 右下角
Alignment(0.0, -0.5) 自定义位置(横向居中,纵向靠上)

widthFactor / heightFactor

  • 默认情况下,Align 会尽量 占满父容器
  • 如果指定 widthFactorheightFactorAlign 的大小会等于 子组件大小 × 对应 factor
  • 常用场景:让 Align 自己的尺寸收缩到子组件的大小附近。

举例说明

class AlignPage extends StatelessWidget {
  const AlignPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Align(
        alignment: Alignment.bottomLeft,
        child: Container(width: 100, height: 100, color: Colors.amber),
      ),
    );
  }
}

默认情况下,Align 会尽量 占满父容器

class AlignPage extends StatelessWidget {
  const AlignPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Align(
        widthFactor: 2,
        heightFactor: 2,
        alignment: Alignment.bottomLeft,
        child: Container(width: 100, height: 100, color: Colors.amber),
      ),
    );
  }
}

如果指定 widthFactorheightFactorAlign 的大小会等于 子组件大小 × 对应 factor

  • Align=子组件大小 * (2*2)

Alignment

Alignment一个二维坐标系上的位置描述类Alignment extends AlignmentGeometry)。

用来指定 子组件在父组件内部的位置

最常用在 AlignStackFractionalOffset 等需要控制位置的地方。

Alignment 的坐标是 相对父容器 的:

  • x 方向:-1.0(最左) → 0.0(中间) → 1.0(最右)
  • y 方向:-1.0(最上) → 0.0(中间) → 1.0(最下)
(-1,-1)   (0,-1)   (1,-1)
topLeft   topCenter topRight

(-1, 0)   (0, 0)   (1, 0)
centerLeft center  centerRight

(-1, 1)   (0, 1)   (1, 1)
bottomLeft bottomCenter bottomRight

以下是Alignment定义的常量:

/// The top left corner.
static const Alignment topLeft = Alignment(-1.0, -1.0);

/// The center point along the top edge.
static const Alignment topCenter = Alignment(0.0, -1.0);

/// The top right corner.
static const Alignment topRight = Alignment(1.0, -1.0);

/// The center point along the left edge.
static const Alignment centerLeft = Alignment(-1.0, 0.0);

/// The center point, both horizontally and vertically.
static const Alignment center = Alignment(0.0, 0.0);

/// The center point along the right edge.
static const Alignment centerRight = Alignment(1.0, 0.0);

/// The bottom left corner.
static const Alignment bottomLeft = Alignment(-1.0, 1.0);

/// The center point along the bottom edge.
static const Alignment bottomCenter = Alignment(0.0, 1.0);

/// The bottom right corner.
static const Alignment bottomRight = Alignment(1.0, 1.0);

我们也可以自定义位置:Alignment(x, y)

class AlignPage extends StatelessWidget {
  const AlignPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Align(
        widthFactor: 2,
        heightFactor: 2,
        alignment: Alignment(0.5, -0.5), // 自定义位置
        child: Container(width: 100, height: 100, color: Colors.amber),
      ),
    );
  }
}

FractionalOffset

FractionalOffset,它其实就是 Alignment 的一个特殊实现版本,用比例来表示子组件在父组件中的位置。

FractionalOffset 这种方式是固定从左上角出发

FractionalOffset(double dx, double dy)

  • dx: 水平方向比例,0.0 = 左边,1.0 = 右边
  • dy: 垂直方向比例,0.0 = 顶部,1.0 = 底部
class AlignPage extends StatelessWidget {
  const AlignPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Align(
        widthFactor: 2,
        heightFactor: 2,
        alignment: FractionalOffset(0, 0),
        child: Container(width: 100, height: 100, color: Colors.amber),
      ),
    );
  }
}

Center

Center 是集成了 Align 对象,默认 alignment=Alignment.center

Center 定义, 少了一个 alignment 参数

class Center extends Align {
  /// Creates a widget that centers its child.
  const Center({ Key? key, double? widthFactor, double? heightFactor, Widget? child })
    : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}

然后 Align 定义, 默认了 this.alignment = Alignment.center,

class Align extends SingleChildRenderObjectWidget {
  /// Creates an alignment widget.
  ///
  /// The alignment defaults to [Alignment.center].
  const Align({
    Key? key,
    this.alignment = Alignment.center, // 默认使用了
    this.widthFactor,
    this.heightFactor,
    Widget? child,
  }) : assert(alignment != null),
       assert(widthFactor == null || widthFactor >= 0.0),
       assert(heightFactor == null || heightFactor >= 0.0),
       super(key: key, child: child);

×

喜欢就点赞,疼爱就打赏