Flutter Scaffold 组件总结
概述
Scaffold 是 Flutter 中实现 Material Design 布局的基础组件,提供了应用程序页面的基本视觉结构和布局框架。它作为一个容器,组合了多个常用的UI元素,如应用栏(AppBar)、主内容区域(body)、悬浮操作按钮(FloatingActionButton)、抽屉(Drawer)等,确保应用程序界面符合 Material Design 规范。
原理说明
1. 设计理念
Scaffold 基于 Material Design 的设计原则,提供了一个标准化的页面布局结构:
- 层次化布局:通过组合不同的子组件,形成清晰的视觉层次
- 一致性:确保应用程序在不同页面间保持一致的布局和交互模式
- 可扩展性:支持灵活的组件组合,满足不同场景的需求
2. 布局管理
Scaffold 内部使用 CustomMultiChildLayout 来管理各个子组件的位置和大小:
- AppBar 固定在页面顶部
- Body 占据主要内容区域
- FloatingActionButton 悬浮在指定位置
- Drawer 从侧边滑出
- BottomNavigationBar 固定在页面底部
3. 状态管理
Scaffold 维护一个 ScaffoldState 对象,管理:
- 抽屉的开关状态
- SnackBar 的显示和隐藏
- 底部表单的显示
- 悬浮操作按钮的动画
构造函数
Scaffold({
Key? key, // 组件的唯一标识符
PreferredSizeWidget? appBar, // 页面顶部的应用栏,通常是AppBar
Widget? body, // 页面的主要内容区域
Widget? floatingActionButton, // 悬浮操作按钮,通常用于主要操作
FloatingActionButtonLocation? floatingActionButtonLocation, // 悬浮按钮的位置
FloatingActionButtonAnimator? floatingActionButtonAnimator, // 悬浮按钮的动画器
List<Widget>? persistentFooterButtons, // 持久化底部按钮列表
Widget? drawer, // 左侧抽屉菜单
DrawerCallback? onDrawerChanged, // 抽屉状态改变时的回调函数
Widget? endDrawer, // 右侧抽屉菜单
DrawerCallback? onEndDrawerChanged, // 右侧抽屉状态改变时的回调函数
Widget? bottomNavigationBar, // 底部导航栏
Widget? bottomSheet, // 底部表单,从底部向上滑出
Color? backgroundColor, // Scaffold的背景颜色
bool? resizeToAvoidBottomInset, // 是否调整大小以避免底部插入(如键盘)
bool primary = true, // 是否为主要的Scaffold,影响状态栏处理
DragStartBehavior dragStartBehavior = DragStartBehavior.start, // 拖拽开始行为
bool extendBody = false, // 是否将body扩展到底部导航栏下方
bool extendBodyBehindAppBar = false, // 是否将body扩展到AppBar后面
Color? drawerScrimColor, // 抽屉遮罩层颜色
double? drawerEdgeDragWidth, // 抽屉边缘拖拽区域宽度
bool drawerEnableOpenDragGesture = true, // 是否启用拖拽手势打开左侧抽屉
bool endDrawerEnableOpenDragGesture = true, // 是否启用拖拽手势打开右侧抽屉
String? restorationId, // 状态恢复标识符
})
主要属性
核心属性
| 属性名 | 类型 | 说明 |
|---|---|---|
appBar | PreferredSizeWidget? | 页面顶部的应用栏 |
body | Widget? | 页面的主要内容区域 |
floatingActionButton | Widget? | 悬浮操作按钮 |
drawer | Widget? | 左侧抽屉菜单 |
endDrawer | Widget? | 右侧抽屉菜单 |
bottomNavigationBar | Widget? | 底部导航栏 |
bottomSheet | Widget? | 底部表单 |
配置属性
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
floatingActionButtonLocation | FloatingActionButtonLocation? | FloatingActionButtonLocation.endFloat | 悬浮按钮位置 |
floatingActionButtonAnimator | FloatingActionButtonAnimator? | FloatingActionButtonAnimator.scaling | 悬浮按钮动画器 |
persistentFooterButtons | List<Widget>? | null | 持久化底部按钮 |
resizeToAvoidBottomInset | bool? | true | 是否调整大小以避免底部插入 |
primary | bool | true | 是否为主要Scaffold |
extendBody | bool | false | 是否扩展body到底部 |
extendBodyBehindAppBar | bool | false | 是否扩展body到AppBar后面 |
实现方式
1. 基础用法
import 'package:flutter/material.dart';
class BasicScaffoldExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('基础示例'),
backgroundColor: Colors.blue,
),
body: Center(
child: Text(
'Hello Flutter!',
style: TextStyle(fontSize: 24),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
print('悬浮按钮被点击');
},
child: Icon(Icons.add),
tooltip: '添加',
),
);
}
}
2. 完整功能示例
import 'package:flutter/material.dart';
class FullFeaturedScaffoldExample extends StatefulWidget {
@override
_FullFeaturedScaffoldExampleState createState() => _FullFeaturedScaffoldExampleState();
}
class _FullFeaturedScaffoldExampleState extends State<FullFeaturedScaffoldExample> {
int _selectedIndex = 0;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
void _showSnackBar() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('这是一个SnackBar'),
action: SnackBarAction(
label: '撤销',
onPressed: () {
// 撤销操作
},
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text('完整功能示例'),
backgroundColor: Colors.deepPurple,
actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
// 搜索功能
},
),
IconButton(
icon: Icon(Icons.more_vert),
onPressed: () {
// 更多选项
},
),
],
),
body: IndexedStack(
index: _selectedIndex,
children: [
_buildHomePage(),
_buildBusinessPage(),
_buildProfilePage(),
],
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(
color: Colors.deepPurple,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
child: Icon(Icons.person, size: 40, color: Colors.deepPurple),
),
SizedBox(height: 10),
Text(
'用户名',
style: TextStyle(color: Colors.white, fontSize: 18),
),
Text(
'user@example.com',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
],
),
),
ListTile(
leading: Icon(Icons.home),
title: Text('首页'),
onTap: () {
Navigator.pop(context);
setState(() {
_selectedIndex = 0;
});
},
),
ListTile(
leading: Icon(Icons.settings),
title: Text('设置'),
onTap: () {
Navigator.pop(context);
// 导航到设置页面
},
),
ListTile(
leading: Icon(Icons.help),
title: Text('帮助'),
onTap: () {
Navigator.pop(context);
// 显示帮助信息
},
),
Divider(),
ListTile(
leading: Icon(Icons.logout),
title: Text('退出登录'),
onTap: () {
Navigator.pop(context);
// 执行退出登录
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _showSnackBar,
child: Icon(Icons.message),
tooltip: '显示消息',
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: '首页',
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: '业务',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '个人',
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.deepPurple,
onTap: _onItemTapped,
),
persistentFooterButtons: [
TextButton(
onPressed: () {
// 持久化按钮1
},
child: Text('按钮1'),
),
TextButton(
onPressed: () {
// 持久化按钮2
},
child: Text('按钮2'),
),
],
);
}
Widget _buildHomePage() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.home, size: 80, color: Colors.deepPurple),
SizedBox(height: 20),
Text('首页', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildBusinessPage() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.business, size: 80, color: Colors.deepOrange),
SizedBox(height: 20),
Text('业务', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildProfilePage() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person, size: 80, color: Colors.green),
SizedBox(height: 20),
Text('个人', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
],
),
);
}
}
3. 高级用法 - 自定义Scaffold状态
import 'package:flutter/material.dart';
class CustomScaffoldExample extends StatefulWidget {
@override
_CustomScaffoldExampleState createState() => _CustomScaffoldExampleState();
}
class _CustomScaffoldExampleState extends State<CustomScaffoldExample> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
bool _isDrawerOpen = false;
void _toggleDrawer() {
if (_isDrawerOpen) {
Navigator.of(context).pop();
} else {
_scaffoldKey.currentState?.openDrawer();
}
setState(() {
_isDrawerOpen = !_isDrawerOpen;
});
}
void _showBottomSheet() {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return Container(
height: 200,
child: Column(
children: [
ListTile(
leading: Icon(Icons.share),
title: Text('分享'),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.link),
title: Text('复制链接'),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.edit),
title: Text('编辑'),
onTap: () {
Navigator.pop(context);
},
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text('自定义Scaffold'),
leading: IconButton(
icon: Icon(_isDrawerOpen ? Icons.close : Icons.menu),
onPressed: _toggleDrawer,
),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
Card(
child: ListTile(
title: Text('显示SnackBar'),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('这是一个自定义SnackBar'),
duration: Duration(seconds: 2),
),
);
},
),
),
Card(
child: ListTile(
title: Text('显示底部表单'),
trailing: Icon(Icons.arrow_forward_ios),
onTap: _showBottomSheet,
),
),
Card(
child: ListTile(
title: Text('显示对话框'),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('确认'),
content: Text('你确定要执行这个操作吗?'),
actions: [
TextButton(
child: Text('取消'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text('确定'),
onPressed: () {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('操作已执行')),
);
},
),
],
);
},
);
},
),
),
],
),
),
drawer: Drawer(
child: ListView(
children: [
DrawerHeader(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purple],
),
),
child: Text(
'导航菜单',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
ListTile(
leading: Icon(Icons.home),
title: Text('首页'),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.settings),
title: Text('设置'),
onTap: () {
Navigator.pop(context);
},
),
],
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('扩展悬浮按钮被点击')),
);
},
label: Text('操作'),
icon: Icon(Icons.add),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
resizeToAvoidBottomInset: true,
extendBody: true,
);
}
}
Scaffold状态管理
1. ScaffoldState 访问
// 方式1:使用GlobalKey
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
Scaffold(
key: _scaffoldKey,
// ...
);
// 访问状态
_scaffoldKey.currentState?.openDrawer();
// 方式2:使用Scaffold.of(context)(在子组件中)
Scaffold.of(context).openDrawer();
// 方式3:使用Builder确保正确的context
Builder(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
Scaffold.of(context).openDrawer();
},
child: Text('打开抽屉'),
);
},
);
2. ScaffoldMessenger
// 显示SnackBar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('消息内容'),
action: SnackBarAction(
label: '撤销',
onPressed: () {
// 撤销操作
},
),
),
);
// 隐藏当前SnackBar
ScaffoldMessenger.of(context).hideCurrentSnackBar();
// 移除所有SnackBar
ScaffoldMessenger.of(context).removeCurrentSnackBar();
常见配置选项
1. FloatingActionButton位置
// 预定义位置
FloatingActionButtonLocation.startTop // 左上角
FloatingActionButtonLocation.centerTop // 顶部中央
FloatingActionButtonLocation.endTop // 右上角
FloatingActionButtonLocation.startFloat // 左侧悬浮
FloatingActionButtonLocation.centerFloat // 中央悬浮
FloatingActionButtonLocation.endFloat // 右侧悬浮(默认)
FloatingActionButtonLocation.startDocked // 左侧停靠
FloatingActionButtonLocation.centerDocked // 中央停靠
FloatingActionButtonLocation.endDocked // 右侧停靠
// 自定义位置
class CustomFabLocation extends FloatingActionButtonLocation {
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
// 返回自定义位置
return Offset(100.0, 100.0);
}
}
2. 响应式布局
Scaffold(
appBar: AppBar(title: Text('响应式示例')),
body: LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
// 宽屏布局
return Row(
children: [
Expanded(flex: 1, child: _buildSidebar()),
Expanded(flex: 3, child: _buildMainContent()),
],
);
} else {
// 窄屏布局
return _buildMainContent();
}
},
),
drawer: MediaQuery.of(context).size.width <= 600 ? _buildDrawer() : null,
);
注意事项和最佳实践
1. 性能优化
// 使用const构造函数
const Scaffold(
appBar: const CustomAppBar(),
body: const MainContent(),
);
// 避免在build方法中创建复杂对象
class OptimizedScaffold extends StatelessWidget {
static const _appBar = AppBar(title: Text('优化示例'));
static const _fab = FloatingActionButton(
onPressed: null,
child: Icon(Icons.add),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appBar,
body: MainContent(),
floatingActionButton: _fab,
);
}
}
2. 错误处理
class SafeScaffoldExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('安全示例')),
body: Builder(
builder: (BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () {
try {
// 确保context是Scaffold的子级
Scaffold.of(context).openDrawer();
} catch (e) {
// 处理错误
print('无法打开抽屉: $e');
}
},
child: Text('打开抽屉'),
),
);
},
),
drawer: Drawer(
child: ListView(
children: [
DrawerHeader(child: Text('抽屉')),
ListTile(title: Text('选项1')),
],
),
),
);
}
}
3. 主题和样式
class ThemedScaffoldExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Theme(
data: Theme.of(context).copyWith(
scaffoldBackgroundColor: Colors.grey[100],
appBarTheme: AppBarTheme(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
elevation: 4,
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
),
),
child: Scaffold(
appBar: AppBar(title: Text('主题示例')),
body: Center(child: Text('自定义主题的Scaffold')),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
),
);
}
}
高级属性详解
1. bottomSheet - 底部表单
bottomSheet 属性用于在页面底部显示一个持久的表单面板。
基本用法
import 'package:flutter/material.dart';
class BottomSheetExample extends StatefulWidget {
@override
_BottomSheetExampleState createState() => _BottomSheetExampleState();
}
class _BottomSheetExampleState extends State<BottomSheetExample> {
bool _showBottomSheet = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('bottomSheet 示例'),
backgroundColor: Colors.blue,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'注意:当显示底部表单时,',
style: TextStyle(fontSize: 16),
),
Text(
'这个内容区域会被向上压缩',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
setState(() {
_showBottomSheet = !_showBottomSheet;
});
},
child: Text(_showBottomSheet ? '隐藏底部表单' : '显示底部表单'),
),
],
),
),
bottomSheet: _showBottomSheet
? Container(
height: 150,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
offset: Offset(0, -2),
),
],
),
child: Column(
children: [
Container(
width: 50,
height: 5,
margin: EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(3),
),
),
Text(
'这是一个持久的底部表单',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: () {},
icon: Icon(Icons.share),
label: Text('分享'),
),
ElevatedButton.icon(
onPressed: () {},
icon: Icon(Icons.download),
label: Text('下载'),
),
ElevatedButton.icon(
onPressed: () {},
icon: Icon(Icons.favorite),
label: Text('收藏'),
),
],
),
],
),
)
: null,
floatingActionButton: FloatingActionButton(
onPressed: () {
// 显示模态底部表单作为对比
showModalBottomSheet(
context: context,
builder: (context) => Container(
height: 200,
child: Center(
child: Text(
'这是模态底部表单\n(与 bottomSheet 不同)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
),
),
);
},
child: Icon(Icons.help),
tooltip: '显示模态底部表单对比',
),
);
}
}
2. floatingActionButtonAnimator - 悬浮按钮动画器
控制 FAB 位置变化时的动画效果。
import 'package:flutter/material.dart';
class FABAnimatorExample extends StatefulWidget {
@override
_FABAnimatorExampleState createState() => _FABAnimatorExampleState();
}
class _FABAnimatorExampleState extends State<FABAnimatorExample> {
FloatingActionButtonLocation _fabLocation = FloatingActionButtonLocation.endFloat;
FloatingActionButtonAnimator _fabAnimator = FloatingActionButtonAnimator.scaling;
final List<FloatingActionButtonLocation> _locations = [
FloatingActionButtonLocation.startFloat,
FloatingActionButtonLocation.centerFloat,
FloatingActionButtonLocation.endFloat,
FloatingActionButtonLocation.startDocked,
FloatingActionButtonLocation.centerDocked,
FloatingActionButtonLocation.endDocked,
];
final Map<FloatingActionButtonAnimator, String> _animators = {
FloatingActionButtonAnimator.scaling: '缩放动画',
FloatingActionButtonAnimator.noAnimation: '无动画',
};
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FAB 动画器示例'),
backgroundColor: Colors.purple,
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'动画器类型:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
..._animators.entries.map((entry) => RadioListTile<FloatingActionButtonAnimator>(
title: Text(entry.value),
value: entry.key,
groupValue: _fabAnimator,
onChanged: (value) {
setState(() {
_fabAnimator = value!;
});
},
)),
SizedBox(height: 20),
Text(
'FAB 位置:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Expanded(
child: GridView.count(
crossAxisCount: 2,
childAspectRatio: 3,
children: _locations.map((location) {
String locationName = location.toString().split('.').last;
return Padding(
padding: EdgeInsets.all(4.0),
child: ElevatedButton(
onPressed: () {
setState(() {
_fabLocation = location;
});
},
style: ElevatedButton.styleFrom(
backgroundColor: _fabLocation == location ? Colors.purple : null,
),
child: Text(
locationName,
style: TextStyle(fontSize: 12),
textAlign: TextAlign.center,
),
),
);
}).toList(),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('FAB 被点击了!')),
);
},
child: Icon(Icons.play_arrow),
backgroundColor: Colors.purple,
),
floatingActionButtonLocation: _fabLocation,
floatingActionButtonAnimator: _fabAnimator,
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: '设置'),
],
),
);
}
}
3. persistentFooterButtons - 持久化底部按钮
在页面底部显示持久的操作按钮。
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(title: 'SafeArea 示例', home: FABAnimatorExample());
}
}
class PersistentFooterButtonsExample extends StatefulWidget {
@override
_PersistentFooterButtonsExampleState createState() => _PersistentFooterButtonsExampleState();
}
class _PersistentFooterButtonsExampleState extends State<PersistentFooterButtonsExample> {
bool _showFooterButtons = true;
String _selectedAction = '';
void _performAction(String action) {
setState(() {
_selectedAction = action;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('执行了: $action'),
duration: Duration(seconds: 1),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('持久化底部按钮示例'),
backgroundColor: Colors.green,
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: Text('显示持久化底部按钮'),
value: _showFooterButtons,
onChanged: (value) {
setState(() {
_showFooterButtons = value;
});
},
),
SizedBox(height: 20),
Text(
'模拟表单内容:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
TextField(
decoration: InputDecoration(
labelText: '用户名',
border: OutlineInputBorder(),
),
),
SizedBox(height: 10),
TextField(
decoration: InputDecoration(
labelText: '邮箱',
border: OutlineInputBorder(),
),
),
SizedBox(height: 10),
TextField(
decoration: InputDecoration(
labelText: '电话',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
),
SizedBox(height: 20),
if (_selectedAction.isNotEmpty)
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green),
),
child: Text(
'最后执行的操作: $_selectedAction',
style: TextStyle(
color: Colors.green[800],
fontWeight: FontWeight.bold,
),
),
),
],
),
),
persistentFooterButtons: _showFooterButtons
? [
TextButton.icon(
onPressed: () => _performAction('取消'),
icon: Icon(Icons.cancel, color: Colors.red),
label: Text(
'取消',
style: TextStyle(color: Colors.red),
),
),
TextButton.icon(
onPressed: () => _performAction('保存草稿'),
icon: Icon(Icons.save, color: Colors.orange),
label: Text(
'保存草稿',
style: TextStyle(color: Colors.orange),
),
),
ElevatedButton.icon(
onPressed: () => _performAction('提交'),
icon: Icon(Icons.send),
label: Text('提交'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
),
]
: null,
);
}
}
4. resizeToAvoidBottomInset - 避免底部插入调整
控制当软键盘弹出时页面的调整行为。
import 'package:flutter/material.dart';
class ResizeToAvoidBottomInsetExample extends StatefulWidget {
@override
_ResizeToAvoidBottomInsetExampleState createState() => _ResizeToAvoidBottomInsetExampleState();
}
class _ResizeToAvoidBottomInsetExampleState extends State<ResizeToAvoidBottomInsetExample> {
bool? _resizeToAvoidBottomInset = true;
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('键盘避让示例'),
backgroundColor: Colors.orange,
),
resizeToAvoidBottomInset: _resizeToAvoidBottomInset,
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
Card(
color: Colors.orange[50],
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'resizeToAvoidBottomInset 设置:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
RadioListTile<bool?>(
title: Text('true - 页面调整避开键盘'),
value: true,
groupValue: _resizeToAvoidBottomInset,
onChanged: (value) {
setState(() {
_resizeToAvoidBottomInset = value;
});
},
),
RadioListTile<bool?>(
title: Text('false - 页面不调整,键盘可能遮挡'),
value: false,
groupValue: _resizeToAvoidBottomInset,
onChanged: (value) {
setState(() {
_resizeToAvoidBottomInset = value;
});
},
),
RadioListTile<bool?>(
title: Text('null - 使用系统默认行为'),
value: null,
groupValue: _resizeToAvoidBottomInset,
onChanged: (value) {
setState(() {
_resizeToAvoidBottomInset = value;
});
},
),
],
),
),
),
SizedBox(height: 20),
Text(
'请点击下方输入框测试效果:',
style: TextStyle(fontSize: 16),
),
Spacer(),
// 底部的输入框,用于测试键盘弹出效果
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Column(
children: [
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: '在这里输入文字',
hintText: '点击这里会弹出键盘',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.edit),
),
maxLines: 3,
),
SizedBox(height: 10),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
_controller.clear();
FocusScope.of(context).unfocus();
},
child: Text('清空'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
),
),
),
SizedBox(width: 10),
Expanded(
child: ElevatedButton(
onPressed: () {
if (_controller.text.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('输入内容: ${_controller.text}'),
),
);
}
},
child: Text('提交'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
),
),
],
),
],
),
),
SizedBox(height: 20),
],
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
5. primary - 是否为主要 Scaffold
控制 Scaffold 是否是当前路由的主要 Scaffold,影响状态栏处理。
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class PrimaryScaffoldExample extends StatefulWidget {
@override
_PrimaryScaffoldExampleState createState() => _PrimaryScaffoldExampleState();
}
class _PrimaryScaffoldExampleState extends State<PrimaryScaffoldExample> {
bool _primary = true;
@override
Widget build(BuildContext context) {
return Scaffold(
primary: _primary,
appBar: AppBar(
title: Text('primary 属性示例'),
backgroundColor: Colors.indigo,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: _primary ? Colors.indigo : Colors.transparent,
statusBarIconBrightness: _primary ? Brightness.light : Brightness.dark,
),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
color: Colors.indigo[50],
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'primary 属性说明:',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.indigo[800],
),
),
SizedBox(height: 10),
Text(
'• true: 这是主要的 Scaffold,处理状态栏样式',
style: TextStyle(fontSize: 14),
),
Text(
'• false: 不是主要 Scaffold,不处理状态栏',
style: TextStyle(fontSize: 14),
),
Text(
'• 通常在嵌套 Scaffold 或特殊布局时使用',
style: TextStyle(fontSize: 14),
),
],
),
),
),
SizedBox(height: 20),
SwitchListTile(
title: Text(
'primary 属性',
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
_primary
? '当前是主要 Scaffold (处理状态栏)'
: '当前不是主要 Scaffold (不处理状态栏)',
),
value: _primary,
onChanged: (value) {
setState(() {
_primary = value;
});
},
),
SizedBox(height: 20),
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'当前状态:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
SizedBox(height: 10),
Text('primary: $_primary'),
Text('状态栏处理: ${_primary ? "是" : "否"}'),
Text('MediaQuery.of(context).padding.top: ${MediaQuery.of(context).padding.top}'),
],
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NestedScaffoldExample(),
),
);
},
child: Text('查看嵌套 Scaffold 示例'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
),
),
],
),
),
);
}
}
class NestedScaffoldExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
primary: true, // 外层 Scaffold 是主要的
appBar: AppBar(
title: Text('嵌套 Scaffold 示例'),
backgroundColor: Colors.indigo,
),
body: Column(
children: [
Container(
height: 100,
color: Colors.indigo[100],
child: Center(
child: Text(
'外层 Scaffold (primary: true)',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
Expanded(
child: Scaffold(
primary: false, // 内层 Scaffold 不是主要的
appBar: AppBar(
title: Text('内层 AppBar'),
backgroundColor: Colors.orange,
automaticallyImplyLeading: false,
),
body: Container(
color: Colors.orange[100],
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'内层 Scaffold (primary: false)',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
Text(
'注意:内层 Scaffold 不处理状态栏样式',
textAlign: TextAlign.center,
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
backgroundColor: Colors.orange,
mini: true,
),
),
),
],
),
);
}
}
6. extendBody - 扩展 body 到底部
控制 body 内容是否扩展到底部组件(如 bottomNavigationBar)的下方。
import 'package:flutter/material.dart';
class ExtendBodyExample extends StatefulWidget {
@override
_ExtendBodyExampleState createState() => _ExtendBodyExampleState();
}
class _ExtendBodyExampleState extends State<ExtendBodyExample> {
bool _extendBody = false;
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('extendBody 示例'),
backgroundColor: Colors.teal,
),
extendBody: _extendBody,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.teal[50]!,
Colors.teal[200]!,
],
),
),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'extendBody 属性控制:',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 10),
SwitchListTile(
title: Text('扩展 body 到底部'),
subtitle: Text(
_extendBody
? 'body 内容会延伸到底部导航栏下方'
: 'body 内容在底部导航栏上方结束',
),
value: _extendBody,
onChanged: (value) {
setState(() {
_extendBody = value;
});
},
),
],
),
),
),
SizedBox(height: 20),
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
Icon(
Icons.arrow_downward,
size: 40,
color: Colors.teal,
),
SizedBox(height: 20),
Text(
'主要内容区域',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.teal[800],
),
),
SizedBox(height: 20),
Text(
_extendBody
? '当 extendBody = true 时,\n这个内容区域会延伸到底部导航栏下方,\n创建半透明效果。'
: '当 extendBody = false 时,\n这个内容区域在底部导航栏上方结束,\n不会被遮挡。',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
Spacer(),
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: _extendBody ? Colors.red[100] : Colors.green[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _extendBody ? Colors.red : Colors.green,
),
),
child: Row(
children: [
Icon(
_extendBody ? Icons.visibility : Icons.visibility_off,
color: _extendBody ? Colors.red : Colors.green,
),
SizedBox(width: 10),
Expanded(
child: Text(
_extendBody
? '这部分内容可能被底部导航栏遮挡'
: '这部分内容不会被底部导航栏遮挡',
style: TextStyle(
color: _extendBody ? Colors.red[800] : Colors.green[800],
fontWeight: FontWeight.bold,
),
),
),
],
),
),
],
),
),
),
),
],
),
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
type: BottomNavigationBarType.fixed,
backgroundColor: _extendBody ? Colors.white.withOpacity(0.9) : Colors.white,
selectedItemColor: Colors.teal,
unselectedItemColor: Colors.grey,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: '首页',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: '搜索',
),
BottomNavigationBarItem(
icon: Icon(Icons.favorite),
label: '收藏',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '个人',
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
_extendBody = !_extendBody;
});
},
child: Icon(_extendBody ? Icons.expand_less : Icons.expand_more),
backgroundColor: Colors.teal,
tooltip: '切换 extendBody',
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
);
}
}
7. extendBodyBehindAppBar - 扩展 body 到 AppBar 后面
控制 body 内容是否扩展到 AppBar 的后面。
import 'package:flutter/material.dart';
class ExtendBodyBehindAppBarExample extends StatefulWidget {
@override
_ExtendBodyBehindAppBarExampleState createState() => _ExtendBodyBehindAppBarExampleState();
}
class _ExtendBodyBehindAppBarExampleState extends State<ExtendBodyBehindAppBarExample> {
bool _extendBodyBehindAppBar = false;
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: _extendBodyBehindAppBar,
appBar: AppBar(
title: Text('extendBodyBehindAppBar 示例'),
backgroundColor: _extendBodyBehindAppBar
? Colors.purple.withOpacity(0.8)
: Colors.purple,
elevation: _extendBodyBehindAppBar ? 0 : 4,
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.purple[100]!,
Colors.purple[50]!,
Colors.white,
],
),
),
child: SafeArea(
top: false, // 不使用安全区域,让内容可以延伸到状态栏
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 顶部间距,用于演示内容是否被 AppBar 遮挡
SizedBox(height: _extendBodyBehindAppBar ? 100 : 20),
Card(
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'extendBodyBehindAppBar 控制:',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.purple[800],
),
),
SizedBox(height: 10),
SwitchListTile(
title: Text('扩展 body 到 AppBar 后面'),
subtitle: Text(
_extendBodyBehindAppBar
? 'body 从屏幕顶部开始,可能被 AppBar 遮挡'
: 'body 从 AppBar 下方开始,不会被遮挡',
),
value: _extendBodyBehindAppBar,
onChanged: (value) {
setState(() {
_extendBodyBehindAppBar = value;
});
},
),
],
),
),
),
SizedBox(height: 20),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 演示内容
_buildDemoSection('顶部内容区域', Icons.arrow_upward,
_extendBodyBehindAppBar ? Colors.red : Colors.green),
SizedBox(height: 20),
_buildDemoSection('中间内容区域', Icons.center_focus_strong, Colors.blue),
SizedBox(height: 20),
_buildDemoSection('底部内容区域', Icons.arrow_downward, Colors.orange),
SizedBox(height: 20),
// 实际应用场景示例
Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
image: DecorationImage(
image: NetworkImage('https://picsum.photos/400/200'),
fit: BoxFit.cover,
onError: (exception, stackTrace) {
// 处理图片加载错误
},
),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.7),
],
),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'沉浸式体验',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
Text(
'extendBodyBehindAppBar 常用于创建沉浸式的界面效果',
style: TextStyle(
color: Colors.white,
fontSize: 14,
),
),
],
),
),
),
),
],
),
),
),
],
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
_extendBodyBehindAppBar = !_extendBodyBehindAppBar;
});
},
child: Icon(_extendBodyBehindAppBar ? Icons.vertical_align_bottom : Icons.vertical_align_top),
backgroundColor: Colors.purple,
tooltip: '切换 extendBodyBehindAppBar',
),
);
}
Widget _buildDemoSection(String title, IconData icon, Color color) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color, width: 2),
),
child: Column(
children: [
Icon(icon, size: 40, color: color),
SizedBox(height: 10),
Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
SizedBox(height: 5),
Text(
title == '顶部内容区域'
? (_extendBodyBehindAppBar ? '可能被 AppBar 遮挡' : '不会被 AppBar 遮挡')
: '正常显示区域',
style: TextStyle(
fontSize: 14,
color: color.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
);
}
}
总结
Scaffold 是 Flutter 应用开发中最重要的布局组件之一,它提供了:
- 标准化的页面结构:符合 Material Design 规范
- 丰富的组件支持:AppBar、Drawer、FAB、BottomNavigationBar 等
- 灵活的配置选项:支持各种布局需求和交互模式
- 完善的状态管理:通过 ScaffoldState 管理组件状态
- 良好的扩展性:支持自定义和高级用法
七个重要属性总结
- bottomSheet: 创建持久的底部表单面板
- floatingActionButtonAnimator: 控制 FAB 的动画效果
- persistentFooterButtons: 提供持久的底部操作按钮
- resizeToAvoidBottomInset: 处理软键盘弹出时的布局调整
- primary: 控制是否为主要 Scaffold,影响状态栏处理
- extendBody: 让内容扩展到底部组件下方
- extendBodyBehindAppBar: 让内容扩展到 AppBar 后面