学习内容:
弹性布局:
Expanded(占满剩余空间)、Flex层叠布局:
Stack(组件叠放)、Positioned(固定位置)
实践任务:
用
Row+Expanded做一个 “底部导航栏”:3 个按钮,每个占 1/3 宽度用
Stack做一个 “图片水印”:背景图 + 右上角文字水印
一、前置只是:两种布局的核心场景※
1. 弹性布局(Expanded/Flex)※
核心是“空间分配”——当父组件有剩余空间时,按比例分配给子组件;或子组件超出空间时,按比例收缩。
适用于“组件需要自适应父容器大小”的场景(如:按钮栏、多列文本布局)。
2. 层叠布局(Stack/Positioned)※
核心是“组件叠放”——将多个组件按先后顺序堆叠,后面的组件覆盖前面的。
适用于“组件需要重叠显示”的场景(如:图片上的标签、悬浮按钮、弹窗)。
二、弹性布局:Expanded + Flex※
1. 核心组件关系※
Flex:弹性布局的基础容器,支持水平(Axis.horizontal)或垂直(Axis.vertical)方向,Row和Column本质是继承自Flex的简化版(Row=Flex(direction: Axis.horizontal), Column=Flex(direction: Axis.vertical))。
Expanded:必须作为Flex(或Row/Column)的直接子组件,租用是“强制子组件填充父组件的剩余空间”,通过flex属性控制空间占比。
2. Flex组件核心属性※
| 属性名 | 作用 | 常用值 |
| direction | 布局方向(主轴) | Axis.horizontal(水平)、Axis.vertical(垂直) |
| children | 子组件列表(可包含Expanded或普通组件) | 任意Widget组合 |
| mainAxisAlignment | 主轴对齐方式(同Row/Column) | MainAxisAlignment.start、center等 |
| crossAxisAlignment | 交叉轴对齐方式(同Row/Column) | CrossAxisAlignment.center、stretch等 |
| mainAxisSize | 主轴占用空间(同Row/Column) | MainAxisSize.max(默认占满)、MainAxisSize.min(仅占子组件总大小) |
3. Expanded组件核心属性※
| 属性名 | 作用 | 常用值 |
| flex | 剩余空间分配比例(默认值1) | 整数(如1、2、3) |
| child | 被拉伸的子组件 | 任意Widget(如Container、Text) |
4. 示例代码※
示例1:基础弹性布局(Row + Expanded分配水平空间)※
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Expanded 基础示例')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(flex: 1, child: ElevatedButton(onPressed: () {}, child: const Text('按钮1'))),
const SizedBox(width: 8),
Expanded(flex: 2, child: ElevatedButton(onPressed: () {}, child: const Text('按钮2'))),
SizedBox(width: 8),
Expanded(flex: 1, child: ElevatedButton(onPressed: () {}, child: const Text('按钮3')))
],
),
),
),
);
}
}- 运行效果

示例2:Flex组件的灵活用法(动态切换方向)※
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget{
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Flex组件示例')),
body: Padding(
padding: EdgeInsets.all(16),
child: Flex(
direction: Axis.vertical,
children: [
Expanded(
flex: 2,
child: Container(color: Colors.yellow, child: Center(child: Text('占两份'))),
),
const SizedBox(height: 8),
Expanded(
flex: 1,
child: Container(color: Colors.blue, child: Center(child: Text('占1份'))),
)
],
),
),
),
);
}
}- 运行效果

示例3:嵌套弹性布局(复杂空间分配)※
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget{
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('嵌套弹性布局')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text('嵌套弹性布局示例', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
Expanded(
child: Row(
children: [
Expanded(
flex: 3,
child: Container(color: Colors.lightBlue, child: const Center(child: const Text('左侧(3份)'))),
),
const SizedBox(width: 8),
Expanded(
flex: 2,
child: Container(color: Colors.lightGreen, child: const Center(child: const Text('右侧(2份)'))),
)
],
),
),
const SizedBox(height: 16),
ElevatedButton(onPressed: (){}, child: const Text('确认'))
],
),
),
),
);
}
}- 运行效果

5. 弹性布局常见问题及解决方案※
问题1:Expanded导致子组件溢出※
- 现象:当Expanded的子组件有固定宽高且总大小超过父组件的约束时,会出现溢出警告
- 原因:Expanded强制子组件填充空间,但是子组件的最大大小超过了父组件能提供的空间。
- 解决方案:给子组件添加constraints限制最大大小,或用Wrap替代Row/Column
// 错误示例:子组件固定宽度 200,父组件宽度不足时溢出
Expanded(
child: Container(width: 200, height: 80, color: Colors.red),
)
// 正确示例:限制子组件最大宽度
Expanded(
child: Container(
constraints: const BoxConstraints(maxWidth: 150), // 最大宽度 150
height: 80,
color: Colors.red,
),
)问题2: Expanded不生效(子组件未拉伸)※
- 原因:Expanded必须是Flex(Row/Column/Flex)的直接子组件,若嵌套在其他组件(如Container)中则不生效
- 错误示例
Row(
children: [
Container(
// Expanded 嵌套在 Container 中,不生效
child: Expanded(child: Text("不会拉伸")),
),
],
)- 正确写法
Row(
children: [
Expanded(
// Expanded 是 Row 的直接子组件,生效
child: Container(child: Text("会拉伸")),
),
],
)问题3:flex值为0时效果※
- flex: 0表示不分配剩余空间,子组件仅占自身大小,等同于不使用Expanded(适用于“按需拉伸”的场景)
Row(
children: [
Expanded(flex: 0, child: Container(width: 100, color: Colors.red)), // 仅占 100px 宽度
Expanded(flex: 1, child: Container(color: Colors.blue)), // 占剩余空间
],
)三、层叠布局:Stack + Positioned※
1. 核心组件关系※
- Stack:层叠布局的容器,子组件按“添加顺序”堆叠(后添加的组件在上面),支持两种子组件:
- 普通子组件:默认按Stack的alignment属性对齐(如:居中、左上角)。
- Positioned子组件:通过left/right/top/bottom精准控制位置,必须是Stack的直接子组件。
- Positioned:仅能在Stack中使用,用于“固定位置”,通过左边控制子组件在Stack中的位置和大小。
2. Stack组件核心属性※
| 属性名 | 作用 | 常用值 |
| alignment | 普通子组件的默认对齐方式 | Alignment.center(默认居中)、Alignment.topLeft、Alignment.bottomRight等 |
| fit | 普通子组件的大小适配方式 | StackFit.loose(默认,子组件按自身大小)、 StackFit.expand(子组件占满Stack大小) |
| overflow | 子组件超出Stack范围时的处理方式 | Overfow.clip(默认,裁剪超出部分)、 Overflow.visible(显示超出部分) |
| children | 子组件列表(可包含普通组件和Positioned) | 任意Widget组合 |
3. Positioned组件核心属性 ※
| 属性名 | 作用 | 说明 |
| left/right | 子组件距离Stack左侧/右侧的距离 | 二选一(或都不设,自动适配宽度) |
| top/bottom | 子组件距离Stack顶部/底部的距离 | 二选一(或都不设,自动适配高度) |
| width/height | 子组件的固定宽高 | 若设置,会覆盖left + right / top + bottom计算的大小 |
| child | 被定位的子组件 | 任意Widget(如:Text、Container) |
4. 示例代码※
示例1:基础层叠(普通子组件 + 默认对齐)※
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget{
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Stack 基础示例'),),
body: Padding(
padding: const EdgeInsets.all(16),
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
image: DecorationImage(
image: const NetworkImage('https://upyun.askrabbit.net/avatar.png'),
fit: BoxFit.cover
)
),
),
const Text(
'这是一张图片的标题',
style: TextStyle(
color: Colors.yellow,
fontSize: 22,
fontWeight: FontWeight.bold,
shadows: [Shadow(color: Colors.black, blurRadius: 3)]
),
)
],
),
),
),
);
}
}- 运行效果

示例2:Positioned固定位置(精准定位)※
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget{
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Positioned定位示例'),),
body: Padding(
padding: EdgeInsets.all(16),
child: Stack(
children: [
Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
image: DecorationImage(
image: const NetworkImage('https://upyun.askrabbit.net/avatar.png'),
fit: BoxFit.cover
)
),
),
Positioned(
top: 10, // 距离顶部10px
right: 10, // 距离右侧10px
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: Colors.red,
child: const Text('热门', style: TextStyle(color: Colors.white, fontSize: 12),),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [Color(0x80000000), Color(0x00000000)],
)
),
child: const Text(
'风景图片展示',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
),
)
],
),
),
),
);
}
}- 运行效果

示例3:Stack溢出处理(clipBehavior 属性)※
当子组件超出Stack范围时,默认会被裁剪(Clip.hardEdge),可通过clipBehavior : Clip.none显示超出部分(针对Flutter 3.10+版本)
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text("Stack 溢出处理")),
body: Padding(
padding: const EdgeInsets.all(16),
child: Stack(
clipBehavior: Clip.none, // 显示超出部分
children: [
// 底层:白色容器(固定大小)
Container(
width: 200,
height: 200,
decoration: const BoxDecoration(
color: Colors.white, // 背景色
boxShadow: [
BoxShadow(color: Colors.grey, blurRadius: 3) // 阴影
]
)
),
// 上层:红色容器(超出底层范围)
Positioned(
top: -20, // 向上超出 20px
left: -20, // 向左超出 20px
child: Container(width: 60, height: 60, color: Colors.red, child: const Center(child: Text("NEW", style: TextStyle(color: Colors.white)))),
),
],
),
),
),
);
}
}- 运行效果

- 适用场景:需要实现“悬浮标签”,“气泡提示”等超出父组件范围的UI。
示例4:Stack + Expanded组合(自适应层叠)※
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget{
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Stack + Expanded组合'),),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text('自适应层叠布局', style: TextStyle(fontSize: 18),),
const SizedBox(height: 16,),
Expanded(
child: Stack(
children: [
Container(
color: Colors.yellow[100],
),
const Center(
child: Text('自适应重叠', style: TextStyle(fontSize: 24),),
),
Positioned(
bottom: 16,
right: 16,
child: ElevatedButton(onPressed: () {}, child: const Text('点击按钮')),
)
],
),
)
],
),
),
),
);
}
}- 运行效果

5. 层叠布局常见问题与解决方案※
问题1:Positioned定位失效※
- 原因1:Positioned不是Stack的直接子组件(如:嵌套在Container中)。
- 原因2:同时设置了left和right但未设置width,或者同时设置了top和bottom但未设置height(导致大小冲突)。
问题2:Stack子组件无法居中(普通子组件)※
- 原因:Stack的alignment属性设置错误,或子组件被Positioned包裹(Positioned会忽略alignment)。
问题3:Stack溢出警告※
- 现象:子组件超出Stack范围你,出现黄色警告条。
- 解决方案:
- 扩大Stack大小(如:用Expanded让Stack占满空间)。
- 设置clipBehavior : Clip.none显示超出部分。
- 调整子组件大小或Positioned的坐标
四、综合实战※
- 构建一个电商App常见的“商品卡片”,包含以下元素:
- 商品图片(占满卡片宽度,固定高度)。
- 右上角“折扣标签”(层叠布局,Positioned定位)。
- 商品标题、价格(弹性布局,标题占满剩余空间,价格右对齐)。
- 底部“加入购物车”按钮(占满卡片宽度)。
- 代码参考
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget{
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('商品信息列表'),),
body: Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: Column(
children: [
// 商品卡片(固定宽度,可复用)
_productCard(),
const SizedBox(height: 16,),
_productCard()
],
),
),
),
),
);
}
}
// 商品卡
Widget _productCard() {
return Container(
width: double.infinity, // 占满父组件宽度
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.yellow,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.grey.withValues(alpha: 0.2), blurRadius: 6, offset: Offset(0, 2))],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, // 子组件左对齐
children: [
// 1. 商品图片 + 折扣标签(层叠布局)
Stack(
children: [
// 商品图片
Container(
width: double.infinity,
height: 180,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: const NetworkImage("https://picsum.photos/id/26/600/400"),
fit: BoxFit.cover
)
),
),
// 折扣标签(右上角定位)
Positioned(
top: 10,
right: 10,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: Colors.red,
child: const Text('8折', style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold),),
),
),
],
),
const SizedBox(height: 12,),
// 2. 商品标题 + 价格(弹性布局)
Row(
children: [
Expanded(
child: const Text(
'2025新款冬季羽绒服',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
maxLines: 1, // 最多显示1行
overflow: TextOverflow.ellipsis, // 超出部分用...表示
),
),
const SizedBox(width: 8,),
// 价格:右对齐,不拉伸
const Text(
'¥99',
style: TextStyle(fontSize: 18, color: Colors.red, fontWeight: FontWeight.bold),
)
],
),
const SizedBox(height: 8,),
// 3. 原价
const Text(
'原价:¥129',
style: TextStyle(fontSize: 14, color: Colors.grey, decoration: TextDecoration.lineThrough),
),
const SizedBox(height: 12,),
// 4. 加入购物车按钮(占满宽度)
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 44), // 占满宽度,高度是44
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('加入购物车', style: TextStyle(fontSize: 16),),
)
],
),
);
}- 运行效果
