文本组件
Text
Text的定义
Text Widget,从名字也可以看出,在 Flutter 里是用来负责显示文本信息的一个组件
const Text(
//要显示的文字内容
this.data,
{
//key类似于id
Key key,
//文字显示样式和属性
this.style,
this.strutStyle,
//文字对齐方式
this.textAlign,
//文字显示方向
this.textDirection,
//设置语言环境
this.locale,
//是否自动换行
this.softWrap,
//文字溢出后处理方式
this.overflow,
//字体缩放
this.textScaleFactor,
//最大显示行数
this.maxLines,
//图像的语义描述,用于向Andoid上的TalkBack和iOS上的VoiceOver提供图像描述
this.semanticsLabel,
})
TextStyle 定义
style 属性比较常用,传入的是 TextStyle 对象,我们细看下它可以配置哪些属性样式。
const TextStyle({
//是否继承父类组件属性
this.inherit = true,
//字体颜色
this.color,
//文字大小,默认14px
this.fontSize,
//字体粗细
this.fontWeight,
//字体样式,normal或italic
this.fontStyle,
//字母间距,默认为0,负数间距缩小,正数间距增大
this.letterSpacing,
//单词间距,默认为0,负数间距缩小,正数间距增大
this.wordSpacing,
//字体基线
this.textBaseline,
//行高
this.height,
//设置区域
this.locale,
//前景色
this.foreground,
//背景色
this.background,
//阴影
this.shadows,
//文字划线,下换线等等装饰
this.decoration,
//划线颜色
this.decorationColor,
//划线样式,虚线、实线等样式
this.decorationStyle,
//描述信息
this.debugLabel,
//字体
String fontFamily,
List<String> fontFamilyFallback,
String package,
})
示例
class ComponentPage extends StatelessWidget {
const ComponentPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('组件')),
body: Column(
children: [
Text(
'字体24下划线',
// 文字对齐方式
textAlign: TextAlign.center,
//textScaleFactor: 1.5, // 字体缩放因子,但是过时了
// 使用 TextScaler 来设置字体缩放因子:会读取用户在系统设置里的字体缩放比例,但最多放大到 2 倍。
textScaler: MediaQuery.textScalerOf(
context,
).clamp(maxScaleFactor: 2.0),
style: TextStyle(
// 字体颜色
color: Colors.blue, // 蓝色
// 字体粗细
fontWeight: FontWeight.bold, // 粗体
// 字体大小
fontSize: 24, // 24 号字体
// 字体下划线
decoration: TextDecoration.underline, // 下划线
// 字体下划线颜色
decorationColor: Colors.red, // 红色
// 字体背景色
backgroundColor: Colors.yellow, // 黄色
),
),
Text(
'缩放,Each line here is progressively more opaque. The base color is material.Colors.black, and Color.withOpacity is used to create a derivative color with the desired opacity. The root TextSpan for this RichText widget is explicitly given the ambient DefaultTextStyle, since RichText does not do that automatically. The inner TextStyle objects are implicitly mixed with the parent TextSpans TextSpan.style.',
// textScaleFactor: 1.0,// 过时啦
// 字体缩放因子: 直接缩放2倍
textScaler: TextScaler.linear(2),
textAlign: TextAlign.start, // 左对齐
// softWrap: true 表示文本是否可以换行
softWrap: true,
// maxLines: 5 表示最多显示5行,如果超出则会剪切
maxLines: 5,
// overflow表示文本溢出的处理方式
// ellipsis表示溢出部分用...表示
// fade表示溢出部分用渐变效果表示
// clip表示溢出部分直接裁剪
// 默认的处理方式是clip
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
],
),
);
}
}

富文本
什么是富文本
富文本就是带有多种样式或功能的文字。相比于普通的“单一文字 + 单一样式”,富文本可以在一段话里包含 不同字体、颜色、大小、下划线、超链接、甚至图片。
Hello Flutter World
- 普通文本:整段文字都是一样的样式,比如都是黑色、18号字。
- 富文本:
- “Hello” → 黑色、18号字
- “Flutter” → 蓝色、加粗、24号字
- “World” → 红色、带下划线
TextSpan
本质是一个 文字片段,可以设置独立的文字内容和样式。
支持嵌套(一个 TextSpan
可以有 children
)。
不能单独显示,必须放在 RichText
或 Text.rich
里使用。
它的定义如下:
const TextSpan({
//样式片段
this.style,
//要显示的文字
this.text,
//样式片段TextSpan数组,可以包含多个TextSpan
this.children,
//用于手势进行识别处理,如点击跳转
this.recognizer,
})
RichText
是 Flutter 底层的 富文本组件,用来展示由 TextSpan
构成的复杂文字。
可以让同一段文字的不同部分有不同样式。
它的定义如下:
const RichText({
Key key,
// 样式片段标签TextSpan
@required this.text,
this.textAlign = TextAlign.start,
this.textDirection,
this.softWrap = true,
this.overflow = TextOverflow.clip,
this.textScaleFactor = 1.0,
this.maxLines,
this.locale,
this.strutStyle,
})
Text.rich
是 Text
组件的一个 命名构造函数。
内部其实就是帮你创建了 RichText
,语法更简洁。
适合在只需要展示富文本时使用,不必直接写 RichText
。
它的定义如下:
const Text.rich(
// 样式片段标签TextSpan
this.textSpan,
{
Key key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
})
案例展示
RichText
RichText(
text: TextSpan(
text: 'Hello ',
style: TextStyle(color: Colors.black, fontSize: 18),
children: [
TextSpan(
text: 'Flutter',
style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
),
TextSpan(
text: ' World!',
style: TextStyle(color: Colors.red),
),
],
),
)
Text.rich
Text.rich(
TextSpan(
text: 'TextSpan',
style: TextStyle(color: Colors.red, fontSize: 24.0),
children: <TextSpan>[
TextSpan(
text: 'aaaaa',
style: TextStyle(color: Colors.blueGrey),
),
TextSpan(
text: 'bbbbbb',
style: TextStyle(color: Colors.cyan),
),
TextSpan(
text: 'Tap点击',
style: const TextStyle(color: Colors.blueGrey),
// 点击手势
recognizer: TapGestureRecognizer()
..onTap = () {
//增加一个点击事件
print('被点击了');
},
),
],
),
),

导入资源
如何导入资源(图片)
创建资源目录
在更目录下创建 assets目录(名字也可以叫做其他,但是我们约定俗成),然后按资源类型,分别创建目录

编辑 pubspec.yaml 文件
flutter:
# To add assets to your application, add an assets section, like this:
assets:
- assets/images/
- assets/icons/
- assets/fonts/
- assets/svg/
加入图片
在images
下面创建3.0x
的文件夹,把图片放进去。接着在利用Flutter GetX Generator
这个插件,生成2.0
和1.0
的图片

assets/
images/
icon.png # 默认 1x
2.0x/icon.png # 2x 屏幕用
3.0x/icon.png # 3x 屏幕用
还生成了一个file.txt
的内容,里面有图片的路径
static const 13399635147767705Jpg = 'assets/images/13399635147767705.jpg';
static const welcomePng = 'assets/images/welcome.png';
创建自己的资源静态类
/// 图片资源
class AssetsImages {
static const pg = 'assets/images/13399635147767705.jpg';
static const welcomePng = 'assets/images/welcome.png';
}
读取图片
class ComponentPage extends StatelessWidget {
const ComponentPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('组件')),
body: Column(children: [Image.asset(AssetsImages.pg, height: 600)]),
);
}
}

1x / 2x / 3x
什么是 1x / 2x / 3x
1x、2x、3x 指的是资源图片的缩放倍数(DPI 密度适配)。
在手机开发中(iOS、Android、Flutter),屏幕分辨率和像素密度不同,同一张图片如果不区分,会导致:
- 在低分辨率屏幕上太清晰(浪费内存)
- 在高分辨率屏幕上模糊
于是就有了多套图片资源:
- 1x → 普通屏幕(低像素密度)
- 2x → Retina 屏幕(像素密度大约是普通屏幕的 2 倍)
- 3x → 更高密度屏幕(像素密度约 3 倍,比如部分安卓高端机)
Flutter 的自动适配
在 Flutter 中:
- 你只需要在
pubspec.yaml
里声明一次图片资源 - Flutter 会根据当前设备的屏幕 DPI,自动选择合适的图片(1x、2x、3x)
目录结构示例:
assets/
images/
icon.png # 默认 1x
2.0x/icon.png # 2x 屏幕用
3.0x/icon.png # 3x 屏幕用
pubspec.yaml
引用:
flutter:
assets:
- assets/images/icon.png
使用时:
Image.asset('assets/images/icon.png')
运行时,Flutter 会自动选择:
- 在 1x 屏幕 → 用
icon.png
- 在 2x 屏幕 → 用
2.0x/icon.png
- 在 3x 屏幕 → 用
3.0x/icon.png
逻辑像素
大多数 UI 设计工具(Figma、Sketch、XD)都会选定一个基准屏幕尺寸来做设计,比如:
- iOS:常用基准是 iPhone 6/7/8 (375pt × 667pt),以 1x = 1pt = 1逻辑像素 作为参考。
- Android:一般以 360dp × 640dp 作为设计基准,1x = 1dp = 1逻辑像素。
所以设计师看到的 50×50,在我们开发里就是 50逻辑像素。
在 Sketch / Figma 里,可以一键导出 @1x、@2x、@3x
在 PS 里,可以通过切片导出多倍图
例如:设计稿上标注一个图标是 50×50
- 导出 @1x → 50×50 像素
- 导出 @2x → 100×100 像素
- 导出 @3x → 150×150 像素
逻辑像素 vs 物理像素
- Flutter 使用的是 逻辑像素(dp/pt) 来布局。
- 真机屏幕有 设备像素比 DPR(devicePixelRatio),决定了「1逻辑像素 = 几个物理像素」。
例如:
- iPhone 11(828×1792 像素,DPR=2) → 屏幕逻辑大小就是 414×896 dp
- 小米某高端机(1080×2400 像素,DPR≈3) → 屏幕逻辑大小大概 360×800 dp
公式:物理像素 = 逻辑像素 × DPR
- 360dp × 1 = 360px → 1x
- 360dp × 2 = 720px → 2x
- 360dp × 3 = 1080px → 3x
一张图片如何判断
一张1080×2399
的图片
- 1080 ÷ 360 = 3
- 说明这张图正好是 基准宽度 360dp 的 3 倍
- 所以它是 3x 图。
同理,高度 2399 ÷ 800 ≈ 3,也符合 3 倍关系。
图片组件Image
Image
构造函数
默认构造函数
Image 是一个图像的 Widget ,提供了一些类方法来快捷使用来自内存、本地、网络、Assets 的图片
以下是他的定义:
//通过ImageProvider来加载图片
const Image({
Key key,
// ImageProvider,图像显示源
@required this.image,
this.semanticLabel,
this.excludeFromSemantics = false,
//显示宽度
this.width,
//显示高度
this.height,
//图片的混合色值
this.color,
//混合模式
this.colorBlendMode,
//缩放显示模式
this.fit,
//对齐方式
this.alignment = Alignment.center,
//重复方式
this.repeat = ImageRepeat.noRepeat,
//当图片需要被拉伸显示的时候,centerSlice定义的矩形区域会被拉伸,类似.9图片
this.centerSlice,
//类似于文字的显示方向
this.matchTextDirection = false,
//图片发生变化后,加载过程中原图片保留还是留白
this.gaplessPlayback = false,
//图片显示质量
this.filterQuality = FilterQuality.low,
})
Image.asset
用于加载 应用资源目录(assets)里的图片。
以下是他的定义:
// 加载本地资源图片,例如项目内资源图片
// 需要把图片路径在pubspec.yaml文件中声明一下,如:
// assets:
// - packages/fancy_backgrounds/backgrounds/background1.png
// 封装类有:AssetImage、ExactAssetImage
Image.asset(
//文件名称,包含路径
String name,
{
Key key,
// 用于访问资源对象
AssetBundle bundle,
this.semanticLabel,
this.excludeFromSemantics = false,
double scale,
this.width,
this.height,
this.color,
this.colorBlendMode,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.centerSlice,
this.matchTextDirection = false,
this.gaplessPlayback = false,
String package,
this.filterQuality = FilterQuality.low,
})
特点:
需要在
pubspec.yaml
里声明:flutter: assets: - assets/images/logo.png
打包进应用,不依赖网络。
常用于 UI 图标、背景图等固定资源。
Image.asset(
'assets/images/logo.png',
width: 100,
height: 100,
fit: BoxFit.cover,
)
Image.network
用于加载 网络图片。
以下是他的定义:
// 加载网络图片,封装类:NetworkImage
Image.network(
//路径
String src,
{
Key key,
//缩放
double scale = 1.0,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
this.height,
this.color,
this.colorBlendMode,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.centerSlice,
this.matchTextDirection = false,
this.gaplessPlayback = false,
this.filterQuality = FilterQuality.low,
Map<String, String> headers,
})
特点:
- 适合显示动态内容(比如用户头像、商品图片)。
- 需要网络请求,第一次可能会有延迟。
- 通常要配合 缓存插件(如
cached_network_image
)使用,避免每次都重新下载。
Image.network(
'https://example.com/logo.png',
width: 100,
height: 100,
fit: BoxFit.cover,
)
Image.file
用于加载 本地文(用户的)件系统里的图片。
以下是他的定义:
// 加载本地File文件图片,封装类:FileImage
Image.file(
//File对象
File file,
{
Key key,
double scale = 1.0,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
this.height,
this.color,
this.colorBlendMode,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.centerSlice,
this.matchTextDirection = false,
this.gaplessPlayback = false,
this.filterQuality = FilterQuality.low,
})
特点:
- 图片必须真实存在于本地路径。
- 适合加载用户拍的照片、下载的图片等。
- 可能需要文件权限(Android 需要存储访问权限)。
import 'dart:io';
Image.file(
File('/storage/emulated/0/DCIM/photo.jpg'),
width: 100,
height: 100,
fit: BoxFit.cover,
)
Image.memory
用于加载 内存中的二进制数据(Uint8List
)。
// 加载Uint8List资源图片/从内存中获取图片显示
// 封装类:MemoryImage
Image.memory(
// Uint8List资源图片
Uint8List bytes,
{
Key key,
double scale = 1.0,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
this.height,
this.color,
this.colorBlendMode,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.centerSlice,
this.matchTextDirection = false,
this.gaplessPlayback = false,
this.filterQuality = FilterQuality.low,
})
特点:
- 直接渲染二进制数据。
- 常用于 Base64 图片、加密图片、本地缓存的二进制图片。
- 灵活但需要自己管理数据来源。
import 'dart:typed_data';
Uint8List bytes = ...; // 可能来自网络/数据库/压缩后的图片数据
Image.memory(
bytes,
width: 100,
height: 100,
fit: BoxFit.cover,
)
关系
Image.asset
、Image.network
、Image.file
、Image.memory
其实都是对 Image
默认构造函数 的封装,分别对应不同的 ImageProvider
:
Image.asset
→ 内部用AssetImage
Image.network
→ 内部用NetworkImage
Image.file
→ 内部用FileImage
Image.memory
→ 内部用MemoryImage
等价写法对比:它们效果完全一样,只是写法不同。
// 方式 1:用 Image.asset
Image.asset('assets/images/logo.png')
// 方式 2:用 Image + AssetImage
Image(image: AssetImage('assets/images/logo.png'))
构造函数 | 数据来源 | 使用场景 |
---|---|---|
Image.asset |
应用打包资源 | App 内置图标、背景、装饰图 |
Image.network |
网络 URL | 头像、商品图、新闻配图 |
Image.file |
本地文件 | 相册照片、下载文件 |
Image.memory |
内存二进制数据 | Base64 图片、缓存字节流 |
colorBlendMode 混合参数
在 Flutter 的 Image
组件中,有一个参数 colorBlendMode
,它的作用就是 定义 color
和图片如何混合(Blend)。
Image
有两个相关参数:
color
:要叠加在图片上的颜色。colorBlendMode
:颜色和图片如何叠加的规则。
枚举 BlendMode 定义
enum BlendMode {
clear,src,dst,srcOver,dstOver,srcIn,dstIn,srcOut,dstOut,srcATop,dstATop,xor,plus,modulate,screen,overlay,darken,lighten,colorDodge,colorBurn,hardLight,softLight,difference,exclusion,multiply,hue,saturation,color,luminosity,
}
效果图:

BlendMode | 解释 | 效果举例(假设图片是 A,颜色是 B) |
---|---|---|
Clear | 清空 | 全部变透明,什么都不显示 |
Src | 仅显示源(颜色 B) | 图片完全被颜色覆盖,只保留 B |
Dst | 仅显示目标(图片 A) | 忽略颜色,只显示图片本身 |
SrcOver | 源覆盖在目标之上 | B 叠加在 A 上(最常用的「覆盖」效果) |
DstOver | 目标覆盖在源之上 | A 叠加在 B 上,颜色在下层 |
SrcIn | 源在目标的交集区域显示 | B 只在 A 的不透明区域可见(图标染色常用 ✅) |
DstIn | 目标在源的交集区域显示 | A 只在 B 的不透明区域可见 |
SrcOut | 源在目标以外显示 | B 只在 A 透明的地方显示 |
DstOut | 目标在源以外显示 | A 只在 B 透明的地方显示 |
SrcATop | 源在目标之上,但裁剪到目标的范围 | B 显示在 A 的区域内,A 仍然显示 |
DstATop | 目标在源之上,但裁剪到源的范围 | A 显示在 B 的区域内,B 仍然显示 |
Xor | 仅显示不重叠部分 | 显示 A 和 B 不相交的部分 |
Darken | 取较暗值 | 对每个像素,取 A 和 B 较暗的结果 |
Lighten | 取较亮值 | 对每个像素,取 A 和 B 较亮的结果 |
Multiply | 颜色相乘 | 结果更暗,类似 Photoshop 的「正片叠底」 |
Screen | 相乘的反效果 | 结果更亮,类似 Photoshop 的「滤色」 |
class ComponentPage extends StatelessWidget {
const ComponentPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(title: const Text('组件')),
body: Column(
children: [
Image.asset(
AssetsImages.welcomePng,
color: Colors.black,
// 颜色混合模式, dstOver 表示在目标图像上叠加源图像
colorBlendMode: BlendMode.dstOver,
),
],
),
);
}
}

fit 图片大小适配
名称 | 说明 |
---|---|
fill | 图片按照指定的大小在 Image 中显示,拉伸显示图片,不保持原比例,填满 Image。 ![]() |
contain | 以原图正常显示为目的,如果原图大小大于 Image 的 size,就按照比例缩小原图的宽高,居中显示在 Image 中。如果原图 size 小于 Image 的 size,则按比例拉升原图的宽和高,填充 Image 一边并居中显示。 ![]() |
cover | 以原图填满 Image 为目的,如果原图 size 大于 Image 的 size,按比例缩小,居中显示在 Image 上。如果原图 size 小于 Image 的 size,则按比例拉升原图的宽和高,填充 Image 居中显示。 ![]() |
fitWidth | 以原图正常显示为目的,如果原图宽大小大于(小于)Image 的宽,就缩小(放大)原图的宽与 Image 一致,居中显示在 Image 中。 ![]() |
fitHeight | 以原图正常显示为目的,如果原图高大小大于(小于)Image 的高,就缩小(放大)原图的高与 Image 一致,居中显示在 Image 中。 ![]() |
none | 保持原图的大小,显示在 Image 的中心。当原图的 size 大于 Image 的 size 时,多出来的部分被截掉。 ![]() |
scaleDown | 以原图正常显示为目的,如果原图大小大于 Image 的 size,就按照比例缩小原图的宽高,居中显示在 Image 中。如果原图 size 小于 Image 的 size,则不做处理居中显示图片。 ![]() |
class ComponentPage extends StatelessWidget {
const ComponentPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(title: const Text('组件')),
body: Column(
children: [
// 设定 Image 的size
SizedBox(
height: 200,
width: 100,
child: Image.asset(
AssetsImages.welcomePng,
color: Colors.black,
// 颜色混合模式, dstOver 表示在目标图像上叠加源图像
colorBlendMode: BlendMode.dstOver,
// 图片填充方式, cover 表示裁剪并缩放以填满整个容器
fit: BoxFit.cover,
),
),
],
),
);
}
}

ImageProvider 图片对象
Image 组件的 image 参数是一个 ImageProvider
, 这样的设计好处是你的图片对象可以来自于各种方式
ImageProvider
是一个抽象类,实现类有 AssetImage
,FileImage
, MemoryImage
, NetWorkImage
import 'package:flutter/material.dart';
class ImagesPage extends StatelessWidget {
const ImagesPage({Key? key}) : super(key: key);
// ImageProvider
Widget _buildImageProvider() {
return const Image(
image: AssetImage('assets/images/welcome.png'),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: _buildImageProvider()),
);
}
}
图标 Icon
Icon 组件用来显示可缩放的图标,不会像图片一样失真,还能设置颜色。
定义
const Icon(
// IconData 图标数据
this.icon, {
Key? key,
// 尺寸
this.size,
// 颜色
this.color,
// 方向
this.textDirection,
this.semanticLabel,
}) : super(key: key);
开启 pubspec.yaml
# The following section is specific to Flutter.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
使用
icon 预览
可以在下面这个网站找到自己想要的图标
https://fonts.google.com/icons
觉得哪个合适,复制它的名字就可以
代码使用
class ComponentPage extends StatelessWidget {
const ComponentPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(title: const Text('组件')),
body: Center(
child: Column(
children: [
// 添加购物车图标
Icon(
// 购物车图标
Icons.shopping_cart,
// 选择大小
size: 100,
// 选择颜色
color: Colors.blue,
),
],
),
),
);
}
}

苹果 Icon
苹果风格 icon 需要用 CupertinoIcons 对象来访问
https://api.flutter.dev/flutter/cupertino/CupertinoIcons-class.html
Icon(
// 购物车图标
CupertinoIcons.shopping_cart,
// 选择大小
size: 100,
// 选择颜色
color: Colors.blue,
),

按钮组件
常见的 Material 按钮组件
按钮 | 说明 | 示例 |
---|---|---|
ElevatedButton | 带阴影、凸起效果的按钮,适合强调性操作 | 提交、确认 |
TextButton | 扁平按钮,没有阴影,主要用于次要操作 | “取消”、“更多” |
OutlinedButton | 带边框的按钮,没有阴影 | “注册”、“返回” |
IconButton | 只有图标的按钮 | 常见于导航栏、工具栏 |
FloatingActionButton (FAB) | 悬浮按钮,一般用于页面的主要操作 | 新增、编辑 |
ElevatedButton
定义
const ElevatedButton({
Key? key,
// 点击事件
required VoidCallback? onPressed,
// 长按
VoidCallback? onLongPress,
// hover
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
// 样式
ButtonStyle? style,
// 焦点
FocusNode? focusNode,
bool autofocus = false,
Clip clipBehavior = Clip.none,
// 按钮内容
required Widget? child,
})
ButtonStyle 样式定义
class ButtonStyle with Diagnosticable {
/// Create a [ButtonStyle].
const ButtonStyle({
// 文字
this.textStyle,
// 背景色
this.backgroundColor,
// 前景色
this.foregroundColor,
// 鼠标滑过颜色
this.overlayColor,
// 阴影
this.shadowColor,
// 阴影高度
this.elevation,
// 内边距
this.padding,
// 最小尺寸
this.minimumSize,
// 固定 size
this.fixedSize,
// 最大最小尺寸
this.maximumSize,
// 边框
this.side,
// 形状
this.shape,
// 鼠标光标
this.mouseCursor,
// 紧凑程度
this.visualDensity,
// 配置可以按下按钮的区域的尺寸
this.tapTargetSize,
// 定义 [shape] 和 [elevation] 的动画更改的持续时间
this.animationDuration,
// 检测到的手势是否应该提供声音和/或触觉反馈
this.enableFeedback,
// 子元素对齐方式
this.alignment,
// 墨水效果
this.splashFactory,
});
代码
ElevatedButton(
// 按钮点击事件
onPressed: () {},
style: ButtonStyle(
// 背景色
backgroundColor: WidgetStateProperty.all(Colors.yellow),
// 前景色
foregroundColor: WidgetStateProperty.all(Colors.red),
// 鼠标滑过颜色
overlayColor: WidgetStateProperty.all(Colors.blue),
// 阴影颜色
shadowColor: WidgetStateProperty.all(Colors.red),
// 阴影高度
elevation: WidgetStateProperty.all(8),
// 边框
side: WidgetStateProperty.all(
const BorderSide(width: 5, color: Colors.cyan),
),
// 固定尺寸
fixedSize: WidgetStateProperty.all(const Size(200, 100)),
),
child: const Text('ElevatedButton'),
),

其它按钮
// 文字按钮
TextButton(onPressed: () {}, child: const Text('文字按钮')),
// 边框按钮
OutlinedButton(onPressed: () {}, child: const Text('边框按钮')),
// 图标按钮
IconButton(
onPressed: () {},
icon: const Icon(Icons.holiday_village),
iconSize: 50,
color: Colors.amber,
),
// 带图标 TextButton
TextButton.icon(
onPressed: () {},
icon: const Icon(Icons.holiday_village),
label: const Text('带图标 TextButton'),
),
// 带图标 OutlinedButton
OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.holiday_village),
label: const Text('带图标 OutlinedButton'),
),

路由
什么是路由
在计算机领域,**路由(Route)**最早来源于 “路径/导航” 的概念:
- 网络里:路由器(Router)帮数据包找到路径。
- Web 开发里:URL 与页面之间的对应关系叫路由。
- Flutter 里:路由就是 一个页面(Screen/Page)的抽象,以及 页面之间跳转的规则。
在Flutter里面,路由的基本概念
Route(路由)
- 表示一个页面(Screen/Page)。
- 在 Flutter 里,页面通常就是一个
Widget
,比如一个Scaffold
,但必须由Route
来管理。
Navigator(导航器)
- 一个路由管理器,维护一个 栈结构(Stack)。
- 入栈(push)= 打开新页面。
- 出栈(pop)= 返回上一个页面。
MaterialApp
提供路由表(
routes
)、初始路由(initialRoute
)、未知路由处理(onUnknownRoute
)等。
匿名路由
一些概念
匿名路由主要是通过 Push()
Pop()
来操作路由,简单场景也能满足业务

Navigator
是一个路由管理的组件,它提供了打开和退出路由页方。
Future push(BuildContext context, Route route)
压入一个新页面到路由堆栈
bool pop(BuildContext context, [ result ])
压出一个页面出堆栈
MaterialPageRoute
继承自PageRoute
类,PageRoute
类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。
MaterialPageRoute({
// 是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget。
// 我们通常要实现此回调,返回新路由的实例。
WidgetBuilder builder,
// 包含路由的配置信息,如路由名称、是否初始路由(首页)。
RouteSettings settings,
// 默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,
// 如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为 false。
bool maintainState = true,
// 表示新的路由页面是否是一个全屏的模态对话框,
// 在 iOS 中,如果fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)。
bool fullscreenDialog = false,
})
路由传值
- 传递可以在初始新界面对象时通过构造函数压入
- 新界面退出后的返回值通过
Navigator.pop
的参数返回
代码
import 'package:flutter/material.dart';
import 'package:flutter_quickstart_learn/RouterPage.dart';
void main(List<String> args) {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(title: 'Flutter Demo', home: const NavPaged());
}
}
import 'package:flutter/material.dart';
class NavPaged extends StatelessWidget {
const NavPaged({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// 页面标题
appBar: AppBar(title: const Text('NavPaged')),
body: Column(
children: [
Center(
//ElevatedButton : 按钮
child: ElevatedButton(
// onPressed: 按钮点击事件, 异步跳转到详情页
onPressed: () async {
// Navigator.push: 异步跳转到新页面
// 但是因为使用了 await,所以会等待新页面的返回值
var result = await Navigator.push(
context,
// MaterialPageRoute: 创建一个新的路由
MaterialPageRoute(
builder: (context) {
// DetailPaged: 详情页
return const DetailPaged(title: "create a new route");
},
),
);
// 上面会等待页面返回新的值,打印返回值
print("路由返回值: $result");
},
child: const Text("Navigator.push DetailPage"),
),
),
],
),
);
}
}
class DetailPaged extends StatelessWidget {
const DetailPaged({super.key, this.title});
// 参数
final String? title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('DetailPaged')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// 按钮
OutlinedButton(
// onPressed: 按钮点击事件, 异步返回到上一个页面
onPressed: () {
// Navigator.pop: 异步返回到上一个页面
// 为什么是异步的?因为Navigator.pop需要等待页面的销毁过程完成
// static void pop<T extends Object?>(BuildContext context, [T? result])
Navigator.pop(context, "ok");
},
child: const Text('Back'),
),
// 显示传值
Text(title ?? ""),
],
),
),
);
}
}


User NavPaged Navigator DetailPaged
| | | |
| 点击按钮 | | |
|--------------->| | |
| | Navigator.push | |
| |-------------------->| |
| | 创建路由对象 | |
| | |------ 创建并显示 ----->|
| | | |
| | <---- 返回 Future ---| |
| | (await 等待结果) | |
| | | |
| | | |
| | | 用户点击Back按钮 |
| | |<----------------------|
| | | Navigator.pop("ok") |
| | |---------------------->|
| | | 销毁 DetailPaged |
| | | |
| | <----- Future 完成 --| |
| | result = "ok" | |
| | 打印 "路由返回值:ok" | |
| | | |
命名路由
如果在routes里面定义了'/'
,则不能使用home
属性,否则会报错
If the home property is specified, the routes table cannot include an entry for "/", since it would be redundant.
意思是: 如果你在MaterialApp
中指定了home
,那么路由表 (routes
) 里就不能再写'/'
这个路由键,否则会冲突。
Navigator.pushNamed
: 异步跳转到详情页
Navigator.pop
: 返回到上一个页面
void main(List<String> args) {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
// 在这里定义路由的名字
// 注册路由表
routes: {
'/': (context) => const NavPaged(),
'/details': (context) => const DetailPaged(),
},
// 如果在routes里面定义了'/',则不能使用home属性,否则会报错
// home: const NavPaged(),
);
}
}
import 'package:flutter/material.dart';
class NavPaged extends StatelessWidget {
const NavPaged({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// 页面标题
appBar: AppBar(title: const Text('NavPaged')),
body: Column(
children: [
Center(
//ElevatedButton : 按钮
child: ElevatedButton(
// onPressed: 按钮点击事件, 跳转到详情页
onPressed: () async {
// Navigator .pushNamed: 跳转到新页面
// 这里使用了命名路由,跳转到 DetailPaged 页面
/* @optionalTypeArgs
static Future<T?> pushNamed<T extends Object?>(
BuildContext context,
String routeName, {
Object? arguments,
})*/
var result = await Navigator.pushNamed(
context,
// 这里是 /details ,是命名路由的名称
"/details",
// 传递参数到 DetailPaged 页面
arguments: {'title': "create a new route"},
);
// 上面会等待页面返回新的值,打印返回值
print("路由返回值: $result");
},
child: const Text("Navigator.push DetailPage"),
),
),
],
),
);
}
}
class DetailPaged extends StatelessWidget {
// const DetailPaged({super.key, this.title});
const DetailPaged({super.key});
// 参数
// final String? title;
@override
Widget build(BuildContext context) {
// ModalRoute 是 Flutter 中一个类,表示当前页面的路由对象。
// ModalRoute.of(context) 会根据 context 找到当前页面对应的路由。
// ModalRoute.of(context)? : 里面的?表示如果找不到对应的路由,则返回null
// settings: 获取路由的设置
// settings.arguments: 获取路由传递的参数
// <String, dynamic>{}: 如果没有传递参数,则使用一个空的Map作为默认值
// as Map: 将获取到的参数强制转换为Map类型
final arguments =
(ModalRoute.of(context)?.settings.arguments ?? <String, dynamic>{})
as Map;
var title = arguments['title'];
return Scaffold(
appBar: AppBar(title: const Text('DetailPaged')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// 按钮
OutlinedButton(
// onPressed: 按钮点击事件, 返回到上一个页面
onPressed: () {
// Navigator.pop: 返回到上一个页面
// static void pop<T extends Object?>(BuildContext context, [T? result])
Navigator.pop(context, "ok");
},
child: const Text('Back'),
),
// 显示传值
Text(title ?? ""),
],
),
),
);
}
}
onGenerateRoute 手动解析
onGenerateRoute
的原理
- 作用:当
Navigator.pushNamed()
被调用时,Flutter 会先在routes
查找对应的路由;如果没找到,就会调用onGenerateRoute
。 - 好处:可以统一管理所有路由逻辑,包括参数传递、异常处理。
onGenerateRoute
更灵活,可以处理:
- 参数传递
- 未知路由(404 页面)
- 动态路由逻辑(比如权限判断)
一般推荐大项目用 onGenerateRoute
,因为它集中管理路由逻辑,更好维护。
import 'package:flutter/material.dart';
import 'package:flutter_quickstart_learn/RouterPage.dart';
void main(List<String> args) {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
// onGenerateRoute 是路由生成的回调
onGenerateRoute: (settings) {
// settings 是 RouteSettings 对象,里面包含了路由的名称和参数
print("settings.name: ${settings.name}");
// Handle '/'
if (settings.name == '/') {
return MaterialPageRoute(builder: (context) => const NavPaged());
}
// Handle '/details/:id'
var uri = Uri.parse(settings.name!);
if (uri.pathSegments.length == 2 &&
uri.pathSegments.first == 'details') {
String uid = uri.pathSegments[1];
return MaterialPageRoute(builder: (context) => DetailPaged(uid: uid));
}
return MaterialPageRoute(builder: (context) => const UnknownPage());
},
);
}
}
import 'package:flutter/material.dart';
class NavPaged extends StatelessWidget {
const NavPaged({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// 页面标题
appBar: AppBar(title: const Text('NavPaged')),
body: Column(
children: [
Center(
//ElevatedButton : 按钮
child: ElevatedButton(
// onPressed: 按钮点击事件, 异步跳转到详情页
onPressed: () async {
// Navigator .pushNamed: 异步跳转到新页面
// 这里使用了命名路由,跳转到 DetailPaged 页面
/* @optionalTypeArgs
static Future<T?> pushNamed<T extends Object?>(
BuildContext context,
String routeName, {
Object? arguments,
})*/
var result = await Navigator.pushNamed(
context,
// 这里是 /details ,是命名路由的名称
"/details/312312312312",
// 传递参数到 DetailPaged 页面
arguments: {'title': "create a new route"},
);
// 上面会等待页面返回新的值,打印返回值
print("路由返回值: $result");
},
child: const Text("Navigator.push DetailPage"),
),
),
],
),
);
}
}
class DetailPaged extends StatelessWidget {
// const DetailPaged({super.key, this.title});
// const DetailPaged({super.key});
// 参数
// final String? title;
const DetailPaged({super.key, this.uid});
final String? uid;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('DetailPaged')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// 按钮
OutlinedButton(
// onPressed: 按钮点击事件, 异步返回到上一个页面
onPressed: () {
// Navigator.pop: 异步返回到上一个页面
// 为什么是异步的?因为Navigator.pop需要等待页面的销毁过程完成
// static void pop<T extends Object?>(BuildContext context, [T? result])
Navigator.pop(context, "ok");
},
child: const Text('Back'),
),
// 显示传值
Text(uid ?? ""),
],
),
),
);
}
}
class UnknownPage extends StatelessWidget {
const UnknownPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(body: Text('UnknownPage'));
}
}
时序图:
sequenceDiagram participant U as User participant N as NavPaged participant Nav as Navigator participant App as MyApp.onGenerateRoute participant D as DetailPaged U->>N: 点击按钮 N->>Nav: pushNamed("/details/312312312312") Nav->>App: 调用 onGenerateRoute App-->>Nav: 返回 MaterialPageRoute Nav->>D: 加载并渲染 DetailPaged D-->>Nav: Navigator.pop("ok") Nav-->>N: 返回结果 "ok" N->>N: 打印返回值: ok
- User → NavPaged:用户点击按钮触发事件。
- NavPaged → Navigator:调用
pushNamed
。 - Navigator → MyApp.onGenerateRoute:根据路由规则生成页面。
- onGenerateRoute → Navigator:返回
MaterialPageRoute
。 - Navigator → DetailPaged:加载并渲染目标页面。
- DetailPaged → Navigator.pop(“ok”):用户返回时传回结果。
- Navigator → NavPaged:把
"ok"
返回给发起调用的地方。
routes
和onGenerateRoute
对比
特性 | routes (命名路由表) |
onGenerateRoute (动态路由生成) |
---|---|---|
定义方式 | 在 MaterialApp 的 routes 属性中写死一个 Map,{"/": (context) => HomePage(), "/detail": (context) => DetailPage()} |
在 onGenerateRoute 回调函数中,根据 settings.name 动态返回 Route |
初学者友好度 | ✅ 简单直观,适合小项目 | ❌ 相对复杂,需要写 switch 或逻辑判断 |
参数传递 | ⚠️ 不太方便,需要额外写构造函数或 settings.arguments 配合使用 |
✅ 非常方便,直接在 settings.arguments 里传,统一处理 |
扩展性 | ❌ 不灵活,路由必须预定义在 Map 中 | ✅ 高度灵活,可以做权限校验、动态跳转、日志记录 |
错误处理 | ❌ 如果路由未定义会报错,容易崩溃 | ✅ 可以处理未匹配路由,返回 404 页面 |
适用场景 | 小型应用,页面少,逻辑简单 | 中大型应用,需要统一管理、权限控制、参数传递复杂 |
可维护性 | 随着页面增加,routes 表越来越臃肿 |
所有路由逻辑集中在 onGenerateRoute ,更容易维护 |
样式管理
ThemeData 样式对象
常见属性分类
在 Flutter 中,ThemeData
是一个 全局样式配置对象,用于统一定义 App 的颜色、字体、按钮风格、图标大小、卡片样式等。它通常配合 MaterialApp
使用。
分类 | 属性 | 作用 |
---|---|---|
颜色相关 | primaryColor |
AppBar、TabBar、按钮等主要颜色 |
primarySwatch |
一组主色调(Material 设计推荐),会自动生成深浅变化 | |
accentColor (旧,改为 colorScheme.secondary ) |
强调色,用于 FloatingActionButton 等 | |
backgroundColor |
Scaffold 的背景色 | |
scaffoldBackgroundColor |
页面背景色 | |
dividerColor |
Divider 的颜色 | |
亮/暗模式 | brightness |
设置亮色(light)或暗色(dark)主题 |
字体与文本 | fontFamily |
全局字体 |
textTheme |
全局文字样式,如 headline1 、bodyText1 |
|
AppBar | appBarTheme |
定制 AppBar 的样式,如背景色、文字颜色 |
按钮 | buttonTheme (旧) |
按钮的全局样式 |
elevatedButtonTheme |
ElevatedButton 的样式 |
|
textButtonTheme |
TextButton 的样式 |
|
outlinedButtonTheme |
OutlinedButton 的样式 |
|
输入框 | inputDecorationTheme |
定制 TextField 的样式(边框、hint、label) |
卡片 | cardTheme |
全局卡片样式(阴影、圆角、颜色) |
图标 | iconTheme |
图标默认颜色和大小 |
底部导航栏 | bottomNavigationBarTheme |
BottomNavigationBar 的样式 |
primaryColor
vs primarySwatch
primaryColor
- 单一颜色 (
Color
) - 用来指定主题的主色调(AppBar、按钮、进度条等的主要颜色)。
- 只能表示一个固定的颜色,没有深浅变化。
primarySwatch
- 一组颜色 (
MaterialColor
) - 表示一个完整的“颜色调色板”,包含不同深浅的色阶(50、100 … 900)。
- Flutter 在生成主题(比如按钮按下、禁用、悬停时的效果)时,需要用到不同层级的颜色,所以它要求的是
MaterialColor
。
关系:
primaryColor
相当于primarySwatch[500]
。- 也就是说,如果你只设置了
primarySwatch
,primaryColor
会自动取它的中间色(通常是 500)。 - 如果你只设置了
primaryColor
,那么你只给了 Flutter 一个颜色,Flutter 就没法自动生成完整的色阶。
组件 / 场景 | 只设置 primaryColor |
设置 primarySwatch |
---|---|---|
AppBar | 使用固定的 primaryColor |
使用 primarySwatch[500] |
FAB (FloatingActionButton) | 颜色可能跟随 primaryColor ,但禁用/按下时只会加透明度 |
背景用 swatch[500] ,按下时 swatch[700] ,禁用时 swatch[100] |
ElevatedButton | 背景 = primaryColor ,按下时颜色基本不变,只是透明度变化 |
背景用 swatch[500] ,按下时自动变 swatch[700] ,禁用时自动变浅色 swatch[100] |
Switch / Slider / ProgressIndicator | 只会用 primaryColor ,状态变化不明显 |
自动根据 swatch 的不同深浅色阶适配各种状态 |
整体主题色层次 | 没有层次,所有地方都是同一个颜色 | 有 10 个色阶,能适配亮/暗/禁用/按下等场景,层次更丰富 |
primaryColor
:单一颜色 → 主题简单,状态变化弱。primarySwatch
:一整套色阶 → Flutter 能帮你自动适配按钮按下、禁用、悬浮等状态,看起来更专业。
一个案例
import 'package:flutter/material.dart';
import 'package:flutter_quickstart_learn/theme.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Quick Start',
// 样式
theme: ThemeData(
// 主题颜色
primarySwatch: Colors.brown,
// appBar颜色
appBarTheme: ThemeData.light().appBarTheme.copyWith(
// appBar背景色
backgroundColor: Colors.green,
// appBar前景色
// 前景色就是文字和图标的颜色
foregroundColor: Colors.white,
),
// 按钮颜色
elevatedButtonTheme: ElevatedButtonThemeData(
// 按钮样式
style: ElevatedButton.styleFrom(
// 按钮背景色
foregroundColor: Colors.white,
// 按钮前景色
backgroundColor: Colors.amber,
),
),
),
// page
home: const ThemePage(),
// 关闭 debug 标签
debugShowCheckedModeBanner: false,
);
}
}
import 'package:flutter/material.dart';
class ThemePage extends StatelessWidget {
const ThemePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ThemePage')),
body: Center(
child: ElevatedButton(onPressed: () {}, child: const Text('Theme')),
),
);
}
}

使用 Theme.of(context)
在 Widget 内部,可以通过 Theme.of(context)
获取当前主题,然后使用里面的样式:
class ThemePage extends StatelessWidget {
const ThemePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ThemePage')),
body: Center(
child: ElevatedButton(
onPressed: () {},
style: ButtonStyle(
foregroundColor: WidgetStateProperty.all(
// 这里使用Theme.of(context)获取了theme的相关配置
Theme.of(context).appBarTheme.backgroundColor,
),
),
child: const Text('Theme'),
),
),
);
}
}

Color 与 MaterialColor
Color
https://api.flutter.dev/flutter/material/Colors-class.html
Color
是 Flutter 中最基本的颜色类,用于表示一个具体的颜色值。
本质上就是一个 32 位 ARGB 整数值:
- A (Alpha, 透明度)
- R (Red, 红色分量)
- G (Green, 绿色分量)
- B (Blue, 蓝色分量)
Color(0xFF42A5F5) // 蓝色(0xAARRGGBB)
Colors.red // Flutter 内置常量(其实也是一个 Color)
Colors.black.withOpacity(0.5) // 半透明黑色
MaterialColor
https://api.flutter.dev/flutter/material/MaterialColor-class.html
MaterialColor
是 Material Design 颜色系统中的一种特殊颜色类。
它不仅包含一个主颜色,还包含 从浅到深的多个色阶(shade),比如 50、100、200 … 900。
常用于 primarySwatch
,因为 Flutter 需要一整套颜色来生成不同组件的效果。
Colors.blue // MaterialColor,蓝色的主色
Colors.blue[100] // 浅蓝
Colors.blue[900] // 深蓝
Colors.blue
并不是一个单一的 Color
,而是一个 MaterialColor
对象,里面内置了 10 个不同深浅的蓝色。
字符串转 Color
// 字符串转 Color
Color stringToColor(String source) {
return Color(int.parse(source, radix: 16) | 0xFF000000);
}
Color c = stringToColor("40c254");
int.parse(source, radix: 16)
source
是一个字符串,比如"40c254"
。radix: 16
表示把它按 16进制 解析成整数。"40c254"
→0x40C254
→ 十进制 = 4242260。
所以得到的数字是 0x0040C254
。
| 0xFF000000
- 在 Flutter 里,
Color
的构造函数需要一个 32位 ARGB 整数:- 格式是
0xAARRGGBB
AA
= Alpha(透明度)RR
= RedGG
= GreenBB
= Blue
- 格式是
- 但我们传进来的
"40c254"
只有 RGB 部分(6位),没有透明度信息。 | 0xFF000000
的意思是 按位或运算,强行把最高的AA
(透明度)位置成FF
,也就是不透明。
所以 0x0040C254 | 0xFF000000 = 0xFF40C254
。
Color(0xFF40C254)
- 最终生成的
Color
对象就是:- Alpha =
FF
(完全不透明) - Red =
40
(64) - Green =
C2
(194) - Blue =
54
(84)
- Alpha =
也就是一个绿色系的颜色。
字符串转 MaterialColor
// 字符串转 MaterialColor
// 字符串转 MaterialColor
static MaterialColor stringToMaterialColor(String source) {
Color color = stringToColor(source);
List<double> strengths = <double>[.05];
Map<int, Color> swatch = <int, Color>{};
// 'red' is deprecated and shouldn't be used. Use (*.r * 255.0).round() & 0xff.
// color.red 也过期了,应该使用 color.r
final int r = (color.r * 255.0).round() & 0xFF,
g = (color.g * 255.0).round() & 0xFF,
b = (color.b * 255.0).round() & 0xFF;
for (int i = 1; i < 10; i++) {
strengths.add(0.1 * i);
}
for (var strength in strengths) {
final double ds = 0.5 - strength;
swatch[(strength * 1000).round()] = Color.fromRGBO(
r + ((ds < 0 ? r : (255 - r)) * ds).round(),
g + ((ds < 0 ? g : (255 - g)) * ds).round(),
b + ((ds < 0 ? b : (255 - b)) * ds).round(),
1,
);
}
// color.value // 过期
// 官方提示:'value' is deprecated and shouldn't be used.
//Use component accessors like .r or .g, or toARGB32 for an explicit conversion.
//
return MaterialColor(color.toARGB32(), swatch);
}
MaterialColor mc = stringToColor("40c254");
Color color = stringToColor(source)
:调用你之前写的 stringToColor("40c254")
,把字符串转成 Color
对象。上面 "40c254"
会得到 Color(0xFF40C254)
。
List<double> strengths = <double>[.05]
:
.05
就是0.05
(Dart 允许省略小数点前的0
)。所以这行等价于:List<double> strengths = [0.05];
创建了一个列表,列表里面有一个值
0.05
final int r = (color.r * 255.0).round() & 0xFF
前面有一个字符串转成
Color
的类,然后获得这个类的 RGB 值在 Flutter 3.7+,
Color.red
/.green
/.blue
被标记为过期。取而代之的是.r
、.g
、.b
、.a
,它们 返回 0.0 ~ 1.0 的 double,而不是 0~255 的整数。* 255.0
:将0~1
的浮点数映射到0~255
的整数范围.round()
:四舍五入,得到最接近的整数& 0xFF
0xFF
是十六进制表示,等于 255。二进制形式:11111111
(8 位全 1)(x & 0xFF)
的作用:会把整数
x
的 低 8 位保留,高位全部清零。也就是说,只保留0~255
的部分。比如:int x = 300; // 二进制:1 0010 1100 int y = x & 0xFF; // 二进制:0010 1100 = 44
strengths.add(0.1 * i)
:生成 [0.05, 0.1, 0.2, ..., 0.9]
这些比例。
for (var strength in strengths) {
final double ds = 0.5 - strength;
swatch[(strength * 1000).round()] = Color.fromRGBO(
r + ((ds < 0 ? r : (255 - r)) * ds).round(),
g + ((ds < 0 ? g : (255 - g)) * ds).round(),
b + ((ds < 0 ? b : (255 - b)) * ds).round(),
1,
);
}
(strength * 1000).round()
:.round()
是取整函数,它会把一个double
四舍五入成最近的int
。最后得到:0.05 → 50 0.1 → 100 0.2 → 200 ... 0.9 → 900
Color.fromRGBO
:在 Flutter 里,Color
是一个用来表示颜色的类,它有一个工厂构造方法:Color.fromRGBO( int r, // 红色通道 (0~255) int g, // 绿色通道 (0~255) int b, // 蓝色通道 (0~255) double opacity, // 透明度 (0.0 ~ 1.0) )
r / g / b
- 红、绿、蓝的通道值,取值范围是
0 ~ 255
。 - 0 = 没有该通道,255 = 最强。
- 例如
(255, 0, 0)
就是纯红色。
opacity
- 透明度,取值范围
0.0 ~ 1.0
。 0.0
→ 完全透明1.0
→ 完全不透明
- 红、绿、蓝的通道值,取值范围是
ds < 0 ? r : (255 - r)
- 如果
ds < 0
,说明strength
> 0.5(比较偏深色),那么用r
(或者 g / b)去调暗; - 否则(
ds >= 0
,偏浅色),用255 - r
(或者 g / b)去调亮。 - 这样保证生成的色阶既有比原色更亮的版本,也有比原色更暗的版本。
- 如果
((ds < 0 ? r : (255 - r)) * ds)
- 这一步决定偏移的幅度。
ds
越大 → 偏移越明显(更亮 / 更暗)。ds
越小 → 偏移越接近原色。
r + (调整量)
- 调整量 =
((ds < 0 ? r : (255 - r)) * ds).round()
- 如果是调亮(
ds >= 0
),就会在原值上往255
方向靠近; - 如果是调暗(
ds < 0
),就会在原值上往0
方向靠近。
- 调整量 =
return MaterialColor(color.toARGB32(), swatch)
:构造 MaterialColor
:
color.toARGB32()
= 主色(比如0xFF40C254
)swatch
= 不同深浅的变体。
代码
class ColorPage extends StatelessWidget {
const ColorPage({super.key});
@override
Widget build(BuildContext context) {
var c = ColorUtils.stringToColor("FFB822");
var mc = ColorUtils.stringToMaterialColor("5C78FF");
return Scaffold(
body: SizedBox.expand(
child: Column(
children: [
// Color
Container(color: c, height: 50),
// MaterialColor
for (var i = 1; i < 10; i++)
Container(color: mc[i * 100], height: 50),
],
),
),
);
}
}

手势事件
在 Flutter 中,手势事件是用户与界面交互的核心机制之一。Flutter 提供了一套完整的 Gesture 系统,用于检测和响应用户的触摸操作(Tap、滑动、长按、缩放等)。
Gesture(手势):用户在屏幕上做出的动作,如点击、双击、拖动、缩放。
GestureDetector:最常用的手势检测组件,可以在它包裹的 Widget 上监听手势。
Listener:更底层的指针事件监听,可以监听原始的 PointerEvent(按下、移动、抬起)。
GestureDetector
GestureDetector 常用手势
手势类型 | 回调方法 | 说明 |
---|---|---|
单击 | onTap |
用户轻触屏幕 |
双击 | onDoubleTap |
用户快速点击两次 |
长按 | onLongPress |
用户按住屏幕超过一段时间 |
按下 | onTapDown |
点击按下瞬间 |
抬起 | onTapUp |
点击抬起瞬间 |
拖动 | onPanStart / onPanUpdate / onPanEnd |
检测手指拖动(平移) |
滑动 | onHorizontalDrag / onVerticalDrag |
检测水平或垂直滑动 |
缩放 | onScaleStart / onScaleUpdate / onScaleEnd |
检测多指缩放手势 |
拖拽 | onPan* |
手指平移,可组合实现拖拽效果 |
基本示例
import 'package:flutter/material.dart';
// 手势识别: 继承了有状态的组件,因为后续的点击事件会重构UI
class GesturePage extends StatefulWidget {
const GesturePage({super.key});
@override
// createState: 在有状态组件中,创建一个状态对象
State<GesturePage> createState() => _GesturePageState();
}
// 状态类 : _GesturePageState 继承自State<GesturePage>
class _GesturePageState extends State<GesturePage> {
// 定义两个变量,用于存储手势滑动的距离
double? dx, dy;
// GestureDetector
// GestureDetector
Widget _buildView() {
return GestureDetector(
child: Container(color: Colors.amber, width: 200, height: 200),
// 点击
onTap: () {
print('点击 onTap');
},
// 长按
onLongPress: () {
print('长按 onLongPress');
},
// 双击
onDoubleTap: () {
print('双击 onLongPress');
},
// 按下
onPanDown: (DragDownDetails e) {
// DragDownDetails: 这是手势按下时的位置信息
// e.globalPosition: 手势在屏幕上的位置
print("按下 ${e.globalPosition}");
},
// 按下滑动
onPanUpdate: (DragUpdateDetails e) {
// DragUpdateDetails: 这是手势滑动时的位置信息
// e.delta: 手势滑动的距离
// e.delta.dx: 手势在x轴上的滑动距离
// e.delta.dy: 手势在y轴上的滑动距离
setState(() {
// 更新dx和dy的值,dx和dy已经声明在上面的代码里了
// double? dx, dy;
dx = e.delta.dx;
dy = e.delta.dy;
});
},
// 松开
onPanEnd: (DragEndDetails e) {
// DragEndDetails: 这是手势结束时的位置信息
// e.velocity: 手势结束时的速度
print(e.velocity);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SizedBox.expand(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 调用_buildView方法,生成一个GestureDetector手势监视器
_buildView(),
// 显示手势滑动的距离
Text('x: $dx, y: $dy'),
],
),
),
);
}
}
I/flutter ( 3413): 按下 Offset(190.8, 324.3)
I/flutter ( 3413): 点击 onTap
I/flutter ( 3413): 按下 Offset(190.8, 324.3)
I/flutter ( 3413): 长按 onLongPress
I/flutter ( 3413): 按下 Offset(164.6, 300.3)
I/flutter ( 3413): Velocity(0.0, 0.0)

使用GestureDetector必须是有状态的么
不一定必须是有状态(StatefulWidget),但是否需要 StatefulWidget 取决于你是否需要 在手势触发时更新界面(UI)。
使用 StatefulWidget 的场景:当手势触发时,你需要改变界面状态,比如:
- 更新文字显示当前手势类型
- 移动 Widget 的位置(拖动效果)
- 改变颜色、大小等
使用 StatelessWidget 的场景:如果手势只触发一些 不影响界面的逻辑,比如打印日志、调用函数、发送请求,可以用 StatelessWidget。
InkWell
基本概念
用途:响应用户点击手势(单击、长按、双击等),并提供 水波纹动画效果。
包裹 Widget:需要放在 Material 组件的子树下(如 Scaffold、Card、Container),否则水波纹无法显示。
优势:
- 内置水波纹动画,符合 Material Design 风格
- 支持多种手势回调(onTap、onLongPress、onDoubleTap 等)
- 可配合
Ink
或Material
控制背景颜色和圆角
常用属性
属性 | 说明 |
---|---|
onTap |
点击时触发 |
onDoubleTap |
双击时触发 |
onLongPress |
长按时触发 |
borderRadius |
水波纹圆角,通常用于圆角按钮 |
splashColor |
水波纹颜色 |
highlightColor |
点击高亮颜色 |
child |
InkWell 包裹的 Widget |
基本示例
必须在 Material Widget 下
必须在 Material Widget 下
Material(
child: InkWell(...),
)
否则水波纹无法显示。
class InkWellPage extends StatelessWidget {
const InkWellPage({super.key});
Widget _buildView() {
return Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(20),
),
// 这里是 InkWell,但是无法产生水波纹效果,因为没有包裹在 Material 组件中
child: InkWell(
// 点击
onTap: () {
print('点击 onTap');
},
// 水波纹颜色
splashColor: Colors.blue,
// 高亮颜色
highlightColor: Colors.yellow,
// 鼠标滑过颜色
hoverColor: Colors.brown,
//
child: const Text('点我 InkWell', style: TextStyle(fontSize: 50)),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.amber,
child: Center(child: _buildView()),
),
);
}
}
在这里点击了,却没有效果,没有产生水波纹

但是在简单背景(如白色 Scaffold)下会显示,这是因为实际上 InkWell 是画在 Material 上的
使用Material要保持背景透明
class InkWellPage extends StatelessWidget {
const InkWellPage({super.key});
Widget _buildView() {
return Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(20),
),
child: Material(
// 透明背景
// color: Colors.transparent,
child: InkWell(
// 点击
onTap: () {},
// 水波纹颜色
splashColor: Colors.blue,
// 高亮颜色
highlightColor: Colors.yellow,
// 鼠标滑过颜色
hoverColor: Colors.brown,
//
child: const Text('点我 InkWell', style: TextStyle(fontSize: 50)),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.amber,
child: Center(child: _buildView()),
),
);
}
}

我们发现,原本应该是绿色的container变成了白色。这情况是 Container 背景色被 Material “覆盖”了,原因在于 Flutter 的绘制顺序:
Container (绿色背景)
└─ Material (color: transparent)
└─ InkWell
- Container 的背景色是通过
decoration.color
绘制的 - Material 的 color 默认是白色,如果你设置
color: Colors.transparent
,理论上是透明的 - 但是 InkWell 的水波纹效果是绘制在 Material 上的,Material 会在它自己的画布上绘制,这会影响 Container 的可视效果
- 在某些 Flutter 版本中,如果 Material 没有
color
,InkWell 仍然会在 Material 上创建一个“ink canvas”,此时 Container 的背景可能看起来消失了(尤其是使用圆角时)
正确的搭配
Widget _buildView() {
return Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(20), // 圆角
),
child: Material(
// 透明背景
color: Colors.transparent,
borderRadius: BorderRadius.circular(20), // 水波纹圆角裁剪
child: InkWell(
// 点击
onTap: () {
print('点击 onTap');
},
borderRadius: BorderRadius.circular(20), // 水波纹圆角
// 水波纹颜色
splashColor: Colors.blue,
// 高亮颜色
highlightColor: Colors.yellow,
// 鼠标滑过颜色
hoverColor: Colors.brown,
//
child: const Text('点我 InkWell', style: TextStyle(fontSize: 50)),
),
),
);
}

color: Colors.transparent
:让Material
的背景透明borderRadius: BorderRadius.circular(20)
:在父容器Container
上面设置了20
的圆角,如果Material
和InkWell
不设置,那么就会波纹就会溢出,到红色方块的位置。
总结注意事项
必须在 Material Widget 下
Material( child: InkWell(...), )
否则水波纹无法显示。
圆角和 splashColor 一定要配合,否则水波纹可能溢出边界。
InkWell 更适合做“按钮”交互,而 GestureDetector 更适合复杂手势。
输入框 TextField
在 Flutter 中,TextField
是最常用的文本输入控件,用于接收用户的文字输入(单行、多行、密码、数字等)。
常用属性
属性 | 说明 |
---|---|
controller |
TextEditingController 控制输入内容,可获取或设置文本 |
focusNode |
管理焦点,可以手动获取或释放焦点 |
decoration |
InputDecoration ,控制输入框样式(边框、提示文字、图标等) |
keyboardType |
键盘类型(如 TextInputType.text 、TextInputType.number 、TextInputType.emailAddress ) |
obscureText |
是否隐藏文本(密码输入框用) |
maxLength |
最大输入长度 |
maxLines |
最大行数,默认 1,设置为 null 或大于 1 可以多行输入 |
minLines |
最小行数 |
style |
文本样式,TextStyle |
textAlign |
文本对齐方式,TextAlign.left / center / right |
cursorColor |
光标颜色 |
cursorHeight |
光标高度 |
enabled |
是否可编辑 |
readOnly |
是否只读 |
onTap |
点击输入框触发 |
onEditingComplete |
编辑完成触发,不同于 onSubmitted |
inputFormatters |
格式化输入,比如只允许数字或限制长度 |
InputDecoration 常用属性
decoration
用于美化输入框:
属性 | 说明 |
---|---|
hintText |
占位提示文字 |
labelText |
标签文字,浮动在上方 |
helperText |
辅助文字 |
prefixIcon |
前缀图标 |
suffixIcon |
后缀图标,如清除按钮 |
border |
输入框边框样式,OutlineInputBorder / UnderlineInputBorder |
filled |
是否填充背景色 |
fillColor |
填充背景颜色 |
errorText |
错误提示文字 |
基础代码
import 'package:flutter/material.dart';
class InputPage extends StatefulWidget {
const InputPage({super.key});
@override
State<InputPage> createState() => _InputPageState();
}
class _InputPageState extends State<InputPage> {
// 文本消息
String _message = "";
// 输入框控制器
final TextEditingController _controllerName = TextEditingController();
final TextEditingController _controllerPassword = TextEditingController();
// 管理焦点
FocusNode focusNodeName = FocusNode();
FocusNode focusNodePassword = FocusNode();
FocusScopeNode? focusScopeNode;
// 输入框 - 用户名
Widget _buildName() {
return TextField(
// 控制器
controller: _controllerName,
// 焦点
autofocus: true,
// 焦点管理
focusNode: focusNodeName,
// 输入框的样式
decoration: const InputDecoration(
// 输入框的标签文本
labelText: '用户名',
// 输入框的辅助提示文本
hintText: '请输入',
// 输入框的前缀图标
prefixIcon: Icon(Icons.person),
// 输入框的后缀图标
suffixIcon: Icon(Icons.edit),
// 输入框的边框样式
border: OutlineInputBorder(),
),
// 输入改变事件
onChanged: (String value) {
setState(() {
_message = value;
});
},
// 提交回车事件
onSubmitted: (String value) {
// 隐藏键盘
focusScopeNode ??= FocusScope.of(context);
// 请求焦点
focusScopeNode?.requestFocus(focusNodePassword);
},
);
}
// 输入框 - 密码
Widget _buildPassword() {
return TextField(
controller: _controllerPassword,
// 密码显示
obscureText: true,
// 焦点管理
focusNode: focusNodePassword,
// 输入框的样式
decoration: const InputDecoration(
labelText: '密码',
hintText: '请输入',
prefixIcon: Icon(Icons.person),
suffixIcon: Icon(Icons.edit),
border: OutlineInputBorder(),
),
);
}
// 按钮
Widget _buildButton() {
return ElevatedButton(
child: const Text('登录 Now!'),
onPressed: () {
setState(() {
_message =
'name:${_controllerName.text}, pass:${_controllerPassword.text}';
});
},
);
}
// 显示
Widget _buildMessage() {
return Text(_message);
}
@override
void dispose() {
// 释放控制器
_controllerName.dispose();
_controllerPassword.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('InputPage')),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
_buildName(),
const SizedBox(height: 10),
_buildPassword(),
const SizedBox(height: 10),
_buildButton(),
const SizedBox(height: 10),
_buildMessage(),
],
),
),
);
}
}

TextEditingController的作用
综合代码
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(
title: 'FocusNode 示例',
theme: ThemeData(primarySwatch: Colors.blue),
home: const FocusDemoPage(),
);
}
}
class FocusDemoPage extends StatefulWidget {
const FocusDemoPage({super.key});
@override
State<FocusDemoPage> createState() => _FocusDemoPageState();
}
class _FocusDemoPageState extends State<FocusDemoPage> {
// 定义多个 FocusNode
final FocusNode _focusNode1 = FocusNode(); // 用于监听焦点变化
final FocusNode _focusNode2 = FocusNode(); // 主动获取/失去焦点
final FocusNode _usernameFocus = FocusNode(); // 多输入框切换
final FocusNode _passwordFocus = FocusNode();
final FocusNode _focusNode4 = FocusNode(); // 动态 UI 改变
@override
void initState() {
super.initState();
// 1️⃣ 监听焦点变化
// 在初始化时添加监听器
_focusNode1.addListener(() {
if (_focusNode1.hasFocus) {
debugPrint("输入框1 获得了焦点");
} else {
debugPrint("输入框1 失去了焦点");
}
});
// 4️⃣ 动态UI刷新
// 在初始化时添加监听器
_focusNode4.addListener(() {
setState(() {});
});
}
@override
void dispose() {
// 释放 FocusNode
// 如果不释放 FocusNode,可能会导致内存泄漏
_focusNode1.dispose();
_focusNode2.dispose();
_usernameFocus.dispose();
_passwordFocus.dispose();
_focusNode4.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("FocusNode 完整示例")),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1️⃣ 监听焦点变化
const Text(
"1. 监听焦点变化",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
TextField(
focusNode: _focusNode1,
decoration: const InputDecoration(labelText: "输入框1(监听焦点变化)"),
),
const SizedBox(height: 20),
// 2️⃣ 主动控制焦点
const Text(
"2. 主动获取/失去焦点",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
TextField(
focusNode: _focusNode2,
decoration: const InputDecoration(labelText: "输入框2(按钮控制焦点)"),
),
Row(
children: [
ElevatedButton(
onPressed: () {
// 主动获取焦点:调用 requestFocus 方法
// FocusScope.of(context): 这是在获取context对应的 FocusScope
// requestFocus: 请求焦点
// 请求获取焦点之后会出现键盘
// 为什么不能用_focusNode2.requestFocus()?
// 因为 FocusNode 需要在其对应的 BuildContext 中使用
FocusScope.of(context).requestFocus(_focusNode2);
},
child: const Text("获取焦点"),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () {
// 主动失去焦点:调用 unfocus 方法
// 失去焦点之后键盘会收起
_focusNode2.unfocus();
},
child: const Text("失去焦点"),
),
],
),
const SizedBox(height: 20),
// 3️⃣ 多输入框切换
const Text(
"3. 多输入框切换焦点",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
TextField(
focusNode: _usernameFocus,
textInputAction: TextInputAction.next,
decoration: const InputDecoration(labelText: "用户名"),
// 提交回车事件:
// _ 表示当前输入框的内容
onSubmitted: (_) {
// 请求焦点: 密码输入框
FocusScope.of(context).requestFocus(_passwordFocus);
},
),
TextField(
focusNode: _passwordFocus,
obscureText: true,
decoration: const InputDecoration(labelText: "密码"),
),
const SizedBox(height: 20),
// 4️⃣ 动态 UI 改变
const Text(
"4. 根据焦点状态动态改变UI",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
TextField(
focusNode: _focusNode4,
decoration: InputDecoration(
labelText: "输入框4(获取焦点时变色)",
border: const OutlineInputBorder(),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: _focusNode4.hasFocus ? Colors.red : Colors.grey,
width: 2,
),
),
),
),
],
),
),
);
}
}

主要作用:
检测输入框是否获取焦点
_focusNode1.addListener(() { if (_focusNode1.hasFocus) { debugPrint("输入框1 获得了焦点"); } else { debugPrint("输入框1 失去了焦点"); } }); TextField( focusNode: _focusNode1, // 绑定_focusNode1 decoration: const InputDecoration(labelText: "输入框1(监听焦点变化)"), ),
手动获取/释放焦点:有时候我们需要在点击按钮时,让某个输入框自动弹出键盘,或者收起键盘。
TextField( focusNode: _focusNode2, decoration: const InputDecoration(labelText: "输入框2(按钮控制焦点)"), ), Row( children: [ ElevatedButton( onPressed: () { // 主动获取焦点:调用 requestFocus 方法 // FocusScope.of(context): 这是在获取context对应的 FocusScope // requestFocus: 请求焦点 // 请求获取焦点之后会出现键盘 // 为什么不能用_focusNode2.requestFocus()? // 因为 FocusNode 需要在其对应的 BuildContext 中使用 FocusScope.of(context).requestFocus(_focusNode2); }, child: const Text("获取焦点"), ), const SizedBox(width: 10), ElevatedButton( onPressed: () { // 主动失去焦点:调用 unfocus 方法 // 失去焦点之后键盘会收起 _focusNode2.unfocus(); }, child: const Text("失去焦点"), ), ], ),
在多个输入框间切换焦点:比如做表单时,用户输入完“用户名”后,点击下一步会自动跳到“密码”输入框。
TextField( focusNode: _usernameFocus, textInputAction: TextInputAction.next, decoration: const InputDecoration(labelText: "用户名"), // 提交回车事件: // _ 表示当前输入框的内容 onSubmitted: (_) { // 请求焦点: 密码输入框 FocusScope.of(context).requestFocus(_passwordFocus); }, ), TextField( focusNode: _passwordFocus, obscureText: true, decoration: const InputDecoration(labelText: "密码"), ),
结合 UI 做效果:可以根据焦点状态来动态改变输入框样式(比如获取焦点时边框变蓝色)。
TextField( focusNode: _focusNode4, decoration: InputDecoration( labelText: "输入框4(获取焦点时变色)", border: const OutlineInputBorder(), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: _focusNode4.hasFocus ? Colors.red : Colors.grey, width: 2, ), ), ), ),
TextEditingController的作用
综合代码
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(
title: 'TextEditingController Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const TextFieldDemoPage(),
);
}
}
class TextFieldDemoPage extends StatefulWidget {
const TextFieldDemoPage({super.key});
// createState()方法 在构建TextFieldDemoPage之后就执行
@override
State<TextFieldDemoPage> createState() => _TextFieldDemoPageState();
}
class _TextFieldDemoPageState extends State<TextFieldDemoPage> {
// 1. 创建控制器,并设置初始值
final TextEditingController _controller = TextEditingController(
text: "Hello Flutter",
);
@override
void initState() {
super.initState();
// 2. 监听输入框内容变化
_controller.addListener(() {
print("输入内容变化:${_controller.text}");
});
}
@override
void dispose() {
_controller.dispose(); // 避免内存泄漏
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("TextEditingController Demo")),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
TextField(
controller: _controller, // 绑定控制器
decoration: const InputDecoration(
labelText: "请输入内容",
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
// 读取输入内容
ElevatedButton(
onPressed: () {
print("当前输入内容:${_controller.text}");
},
child: const Text("读取内容"),
),
// 修改输入内容
ElevatedButton(
onPressed: () {
_controller.text = "新内容设置成功!";
},
child: const Text("修改内容"),
),
// 控制光标和选中范围
ElevatedButton(
onPressed: () {
_controller.selection = TextSelection(
baseOffset: 0, // 选中范围的起始位置
extentOffset: _controller.text.length, // 选中范围的结束位置
);
},
child: const Text("全选文本"),
),
// 清空输入框
ElevatedButton(
onPressed: () {
_controller.clear();
},
child: const Text("清空内容"),
),
],
),
),
);
}
}

主要作用
获取输入内容:可以通过
controller.text
获取用户在输入框中输入的内容。// 读取输入内容 ElevatedButton( onPressed: () { print("当前输入内容:${_controller.text}"); }, child: const Text("读取内容"), ),
设置输入内容:可以主动给输入框赋值,常用于表单初始化或清空输入框。
// 1. 创建控制器,并设置初始值 final TextEditingController _controller = TextEditingController( text: "Hello Flutter", ); controller: _controller, // 绑定控制器 // 清空输入框 ElevatedButton( onPressed: () { _controller.clear(); }, child: const Text("清空内容"), ),
监听输入变化:通过
addListener
方法可以实时监听输入框内容变化,而不需要依赖onChanged
回调。// 2. 监听输入框内容变化 _controller.addListener(() { print("输入内容变化:${_controller.text}"); }); }
控制光标位置 / 选择文本:
TextEditingController
还能操作光标和选中文本范围。// 控制光标和选中范围 ElevatedButton( onPressed: () { _controller.selection = TextSelection( baseOffset: 0, // 选中范围的起始位置 extentOffset: _controller.text.length, // 选中范围的结束位置 ); }, child: const Text("全选文本"), ), // 光标移动到末尾 _controller.selection = TextSelection.fromPosition( TextPosition(offset: _controller.text.length), );