本章介绍:
-
Text
-
RichText
-
TextSpan
-
SelectableText
-
DefaultTextStyle
-
Icon
-
Image
-
Placeholder
重点解决:
-
为什么文字会溢出
-
为什么
TextOverflow.ellipsis有时不生效 -
Text.rich和RichText应该怎么选 -
文字样式是如何继承的
-
网络图片加载失败如何显示默认图
-
BoxFit.cover和contain有什么区别 -
为什么一张大图可能占用几十 MB 内存
一、Text
1. Text 的作用
Text 用于显示一段文字。
const Text('Hello Flutter');
一段文字最终显示成一行还是多行,不只取决于文字内容,还取决于父组件提供的宽度约束。没有显式设置 style 时,Text 会读取最近的 DefaultTextStyle;如果指定的 TextStyle.inherit 为 true它会和默认样式合并。
2. 常用参数
Text(
'Flutter makes it easy to build beautiful applications.',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
height: 1.4,
),
textAlign: TextAlign.start,
maxLines: 2,
overflow: TextOverflow.ellipsis,
softWrap: true,
);
常用参数:
参数
作用
style
字号、颜色、字重、行高等
textAlign
文字水平对齐
maxLines
最多显示多少行
overflow
超出范围后如何处理
softWrap
是否允许在合适位置自动换行
textDirection
从左到右或从右到左
semanticsLabel
为无障碍功能提供替代说明
textScaler
控制文字缩放方式
当前 Flutter API 中,旧的 textScaleFactor 已被标记为弃用,新的代码应优先使用 textScaler或者让组件遵循系统的文字缩放设置。
二、Text 如何处理换行
1. 父组件提供有限宽度
SizedBox(
width: 160,
child: Text(
'This is a long sentence that can wrap.',
),
);
Text 收到的最大宽度是 160,因此文字会根据可用宽度自动换行。
2. 强制换行
字符串中的 \n 会产生明确换行:
const Text(
'Alice, 32\nLos Angeles, California',
);
结果:
Alice, 32
Los Angeles, California
3. softWrap
const Text(
'This is a very long sentence.',
softWrap: false,
);
softWrap: false 表示不在普通单词边界处自动换行。
但要注意:
softWrap: false不等于文字一定不会被裁剪。
文字是否溢出,仍然取决于父组件约束和 overflow。
三、maxLines 与 overflow
1. maxLines
const Text(
'A very long profile description...',
maxLines: 2,
);
表示最多显示两行。
如果文字超过两行,如何处理由 overflow 决定。Flutter 的 Text API 也明确说明,超过 maxLines 后会按照 overflow 截断。
2. TextOverflow 类型
clip
overflow: TextOverflow.clip
直接裁掉超出的部分。
ellipsis
overflow: TextOverflow.ellipsis
显示省略号:
Flutter makes it easy to...
fade
overflow: TextOverflow.fade
超出位置使用淡出效果。
visible
overflow: TextOverflow.visible
允许文字绘制到原布局范围之外。
这不代表父组件会为它增加空间,可能导致文字覆盖其他组件。
四、为什么 TextOverflow.ellipsis 有时不生效
这是实际项目中最常见的问题之一。
错误示例
Row(
children: [
const Icon(Icons.person),
Text(
longUserName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
);
问题表现
文字仍然溢出,右侧显示黄色和黑色警告条:
A RenderFlex overflowed by XX pixels
原因
Row 会先让普通非 flex 子组件按照自身需要测量宽度。
Text 没有得到一个明确的“最大可用宽度”,因此它可能尝试显示完整内容。
ellipsis 不是主动决定文字宽度的工具。
它只能在 Text 已经收到有限宽度并且确实判断出内容溢出时生效。
正确写法
Row(
children: [
const Icon(Icons.person),
const SizedBox(width: 8),
Expanded(
child: Text(
longUserName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
);
Expanded 把 Row 的剩余宽度作为约束传给 Text。
Text 现在知道:
最多只能使用这么宽
所以省略号才能正确生效。
另一种正确写法
Row(
children: [
const Icon(Icons.person),
const SizedBox(width: 8),
Flexible(
child: Text(
longUserName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
);
区别是:
-
Expanded强制使用全部剩余空间; -
Flexible允许 Text 使用比剩余空间更小的宽度。
五、TextStyle 的继承
1. 完整覆盖样式
const Text(
'Premium Member',
style: TextStyle(
fontSize: 18,
color: Colors.amber,
fontWeight: FontWeight.bold,
),
);
2. 部分继承
const Text(
'Premium Member',
style: TextStyle(
fontWeight: FontWeight.bold,
),
);
默认情况下:
TextStyle.inherit == true
因此这里只覆盖字重。
字号、字体、颜色等没有设置的属性,可以继续从最近的 DefaultTextStyle 获得。
3. 禁止继承
const Text(
'Independent style',
style: TextStyle(
inherit: false,
fontSize: 16,
color: Colors.black,
),
);
使用 inherit: false 后,需要自行提供必要样式。
普通业务中很少需要这样做,因为它可能导致组件不能正确跟随主题。
六、DefaultTextStyle
DefaultTextStyle 为其子树中的 Text 提供默认文字样式。没有显式样式的 Text 会读取这个默认样式。
DefaultTextStyle(
style: const TextStyle(
fontSize: 16,
color: Colors.deepPurple,
height: 1.4,
),
child: Column(
children: const [
Text('Alice'),
Text('Los Angeles'),
Text('Online now'),
],
),
);
三个 Text 都会获得相同的:
-
字号
-
颜色
-
行高
DefaultTextStyle.merge
只希望修改部分默认样式时,可以使用:
DefaultTextStyle.merge(
style: const TextStyle(
fontWeight: FontWeight.bold,
),
child: const Text('Premium Member'),
);
它会保留已有默认样式,只覆盖字重。
实际使用场景
假设一张用户卡片中的所有文字都使用灰色:
DefaultTextStyle.merge(
style: const TextStyle(
color: Colors.black54,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Alice, 32'),
Text('Los Angeles, CA'),
Text('Looking for a meaningful relationship'),
],
),
);
这样比每个 Text 都重复写颜色更容易维护。
七、RichText
1. RichText 的作用
RichText 用于在同一段文字中显示多个不同样式。
它接收的不是普通字符串,而是一棵 InlineSpan 树,通常由多个 TextSpan 组成。RichText 不会像普通 Text 那样自动应用 DefaultTextStyle需要显式提供根样式,或者手动读取当前默认样式。
RichText(
text: const TextSpan(
style: TextStyle(
fontSize: 16,
color: Colors.black,
),
children: [
TextSpan(text: 'Alice is '),
TextSpan(
text: 'online',
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
TextSpan(text: ' now.'),
],
),
);
八、TextSpan
TextSpan 不是一个普通的 Widget。
它是一段不可变的文字描述,可以包含:
-
text -
style -
children -
recognizer -
semanticsLabel
父 TextSpan 的样式会应用到自己的文字和子节点,子 TextSpan 可以只覆盖部分样式。
const TextSpan(
style: TextStyle(
fontSize: 16,
color: Colors.black87,
),
children: [
TextSpan(text: 'By continuing, you agree to the '),
TextSpan(
text: 'Terms of Use',
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
TextSpan(text: ' and '),
TextSpan(
text: 'Privacy Policy',
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
TextSpan(text: '.'),
],
);
九、Text.rich 与 RichText 的区别
Text.rich
const Text.rich(
TextSpan(
children: [
TextSpan(text: 'Hello '),
TextSpan(
text: 'Flutter',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
);
优点:
-
自动继承
DefaultTextStyle -
支持
const -
使用方式更接近普通 Text
-
适合大多数富文本展示
RichText
RichText(
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: const [
TextSpan(text: 'Hello '),
TextSpan(
text: 'Flutter',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
);
优点:
-
对底层文字布局控制更直接
-
可以配置选择系统相关参数
-
更适合高级富文本场景
Flutter 官方建议:单一样式优先使用 Text;需要多样式又希望继承环境样式时,优先考虑 Text.rich需要更底层的控制时再使用 RichText。
选择建议
普通单一样式文字
→ Text
一段文字包含多个样式
→ Text.rich
需要更底层的选择、注册器或渲染控制
→ RichText
十、让 TextSpan 可以点击
例如,让“Terms of Use”可以点击:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class AgreementText extends StatefulWidget {
const AgreementText({super.key});
@override
State<AgreementText> createState() => _AgreementTextState();
}
class _AgreementTextState extends State<AgreementText> {
late final TapGestureRecognizer termsRecognizer;
@override
void initState() {
super.initState();
termsRecognizer = TapGestureRecognizer()
..onTap = () {
debugPrint('Open Terms of Use');
};
}
@override
void dispose() {
termsRecognizer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text.rich(
TextSpan(
children: [
const TextSpan(
text: 'By continuing, you agree to the ',
),
TextSpan(
text: 'Terms of Use',
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: termsRecognizer,
),
const TextSpan(text: '.'),
],
),
);
}
}
TextSpan.recognizer 可以接收点击事件,但 GestureRecognizer 需要由组件正确管理生命周期。
不要每次 build 都创建新的 recognizer:
TextSpan(
text: 'Terms',
recognizer: TapGestureRecognizer()..onTap = openTerms,
);
这种写法不方便释放 recognizer。
十一、SelectableText
1. 作用
SelectableText 允许用户长按或拖动选择、复制文字。
const SelectableText(
'Customer service number: 1-800-000-0000',
);
适合:
-
订单号
-
邮箱
-
地址
-
用户 ID
-
错误日志
-
长文章
-
可复制的联系方式
SelectableText 支持单一样式,也有 SelectableText.rich 用于多样式文本。对于包含多个普通 Text 的较大区域,Flutter 目前还建议考虑使用 SelectionArea 或 SelectableRegion。
2. SelectableText.rich
const SelectableText.rich(
TextSpan(
children: [
TextSpan(text: 'User ID: '),
TextSpan(
text: '93432015',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
);
3. SelectionArea
希望整个页面中的文字都可选择时:
SelectionArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Order number: 123456'),
Text('Email: example@example.com'),
Text('Address: Los Angeles, CA'),
],
),
);
相比把每一段都改成 SelectableText这种方式更适合一整片内容区域。
4. 不适合使用 SelectableText 的场景
按钮文字通常不需要使用:
SelectableText('Continue')
按钮应该让用户点击,不应该让用户误触文字选择。
正确:
ElevatedButton(
onPressed: () {},
child: const Text('Continue'),
);
十二、Icon
1. Icon 的作用
Icon 使用 IconData 对应的字体字形显示图标。
const Icon(Icons.favorite);
Icon 本身没有点击行为。需要可点击图标时,应使用 IconButton或者在合适的 Material 结构中使用 InkWell。
2. 常用参数
const Icon(
Icons.favorite,
size: 28,
color: Colors.red,
semanticLabel: 'Favorite',
);
参数
作用
size
图标尺寸
color
图标颜色
semanticLabel
无障碍说明
shadows
图标阴影
textDirection
图标方向
fill
可变图标填充程度
weight
可变图标笔画粗细
grade
可变图标笔画级别
opticalSize
可变图标光学尺寸
Icon 可以通过 IconTheme 从上层获得默认配置。
十三、IconTheme
IconTheme(
data: const IconThemeData(
color: Colors.deepPurple,
size: 30,
),
child: Row(
children: const [
Icon(Icons.favorite),
Icon(Icons.message),
Icon(Icons.person),
],
),
);
这三个 Icon 都会继承:
color = deepPurple
size = 30
单个 Icon 可以覆盖主题:
const Icon(
Icons.favorite,
color: Colors.red,
);
十四、Icon 与 IconButton 的区别
Icon
const Icon(Icons.close);
只是显示,不负责点击。
IconButton
IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.close),
tooltip: 'Close',
);
IconButton 提供:
-
点击事件
-
按压反馈
-
标准触摸区域
-
tooltip
-
禁用状态
-
Material 样式
不要为了点击直接写:
GestureDetector(
onTap: close,
child: const Icon(Icons.close),
);
这可能导致触摸区域只有图标本身那么小。
十五、Image
1. Image 的作用
Image 用于显示图片。
Flutter 提供了多种构造方式:
-
Image.asset -
Image.network -
Image.file -
Image.memory -
Image(image: imageProvider)
它们分别从资源包、网络、文件和内存字节中获取图片。
十六、Image.asset
用于显示项目中的本地资源:
Image.asset(
'assets/images/default_avatar.png',
width: 80,
height: 80,
fit: BoxFit.cover,
);
需要在 pubspec.yaml 中注册:
flutter:
assets:
- assets/images/
适合:
-
App Logo
-
默认头像
-
空状态图片
-
本地图标
-
固定背景图
十七、Image.network
Image.network(
user.photoUrl,
width: 80,
height: 80,
fit: BoxFit.cover,
);
适合:
-
用户头像
-
动态图片
-
商品图片
-
远程 Banner
-
服务器配置图片
十八、Image.file
import 'dart:io';
Image.file(
File(filePath),
width: 120,
height: 120,
fit: BoxFit.cover,
);
适合显示:
-
用户刚从相册选中的图片
-
拍照结果
-
已下载到本地的文件
Web 平台不能直接使用 dart:io 的 File。
十九、Image.memory
import 'dart:typed_data';
Image.memory(
imageBytes,
width: 120,
height: 120,
fit: BoxFit.cover,
);
其中:
Uint8List imageBytes;
适合:
-
Base64 解码后的图片
-
接口返回的图片字节
-
内存中的截图结果
-
图片编辑结果
二十、BoxFit 详细区别
假设容器为:
200 × 100
图片原始比例为:
100 × 200
也就是一张竖图。
BoxFit
效果
是否变形
是否可能裁剪
fill
强制填满宽高
是
否
contain
完整显示整张图
否
否
cover
填满整个容器
否
是
fitWidth
宽度完整适配
否
高度可能超出
fitHeight
高度完整适配
否
宽度可能超出
none
保持原始尺寸
否
可能
scaleDown
图片过大时缩小
否
通常不裁剪
这些模式的官方定义分别围绕“完整包含图片”“完整覆盖目标区域”“保持宽度或高度完整显示”等行为。
1. BoxFit.cover
Image.network(
photoUrl,
width: 120,
height: 120,
fit: BoxFit.cover,
);
特点:
-
保持宽高比
-
填满整个区域
-
图片部分内容可能被裁掉
最适合:
-
用户头像
-
用户照片卡片
-
商品封面
-
Banner 背景
2. BoxFit.contain
Image.asset(
'assets/images/logo.png',
width: 200,
height: 100,
fit: BoxFit.contain,
);
特点:
-
保持宽高比
-
完整显示整张图片
-
容器可能留白
最适合:
-
Logo
-
产品图片
-
证件图片
-
不能裁剪的重要图片
3. BoxFit.fill
Image.asset(
'assets/images/background.png',
width: 200,
height: 100,
fit: BoxFit.fill,
);
特点:
-
完全填满
-
不保持原始比例
-
可能把人脸拉宽或拉长
用户照片通常不应该使用 fill。
4. fitWidth
fit: BoxFit.fitWidth
保证图片宽度完整适配容器。
图片高度可能超过容器。
适合:
-
长图按屏幕宽度展示
-
文章中的大图
-
图片查看器中的竖图
5. fitHeight
fit: BoxFit.fitHeight
保证图片高度完整适配容器。
图片宽度可能超过容器。
适合:
-
横屏图片查看器
-
需要优先显示完整高度的横图
二十一、BoxFit 不等于裁剪圆角
错误:
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50),
),
child: Image.network(
photoUrl,
fit: BoxFit.cover,
),
);
问题表现
Container 有圆角设置,但图片仍然显示成方形。
原因
borderRadius 只控制背景或边框如何绘制,不会自动裁剪 child。
正确写法
ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.network(
photoUrl,
width: 100,
height: 100,
fit: BoxFit.cover,
),
);
圆形头像也可以使用:
ClipOval(
child: Image.network(
photoUrl,
width: 100,
height: 100,
fit: BoxFit.cover,
),
);
二十二、网络图片加载中状态
Image.network(
photoUrl,
width: 120,
height: 120,
fit: BoxFit.cover,
loadingBuilder: (
BuildContext context,
Widget child,
ImageChunkEvent? loadingProgress,
) {
if (loadingProgress == null) {
return child;
}
final expectedBytes = loadingProgress.expectedTotalBytes;
final loadedBytes = loadingProgress.cumulativeBytesLoaded;
return Center(
child: CircularProgressIndicator(
value: expectedBytes == null
? null
: loadedBytes / expectedBytes,
),
);
},
);
loadingBuilder 用于指定图片加载过程中显示的内容。
二十三、网络图片加载失败
Image.network(
photoUrl,
width: 120,
height: 120,
fit: BoxFit.cover,
errorBuilder: (
BuildContext context,
Object error,
StackTrace? stackTrace,
) {
return Container(
width: 120,
height: 120,
color: Colors.grey.shade200,
alignment: Alignment.center,
child: const Icon(
Icons.person,
size: 48,
color: Colors.grey,
),
);
},
);
errorBuilder 会在图片加载失败时构建替代组件。
实际项目注意事项
不要直接给用户显示:
Text(error.toString())
这可能暴露:
-
URL
-
服务端路径
-
网络库细节
-
技术错误信息
用户看到的应该是:
Photo unavailable
或者直接显示默认头像。
二十四、为什么建议为图片设置 width 和 height
错误:
Image.network(photoUrl);
网络图片下载前,Flutter 可能还不知道最终宽高。
图片加载完成后,页面布局可能发生变化:
加载前:高度很小
加载后:突然撑开
可能造成:
-
列表跳动
-
页面闪动
-
用户误触
-
滚动位置视觉变化
建议:
Image.network(
photoUrl,
width: 120,
height: 120,
fit: BoxFit.cover,
);
或者:
AspectRatio(
aspectRatio: 1,
child: Image.network(
photoUrl,
fit: BoxFit.cover,
),
);
二十五、图片为什么容易占用大量内存
一张图片文件可能只有:
2 MB JPEG
但解码到内存后,通常需要按照像素存储。
例如一张:
4000 × 3000
的图片,按每个像素约 4 字节计算:
4000 × 3000 × 4
= 48,000,000 字节
≈ 45.8 MB
这还只是一张图片。
列表中同时存在多张大图时,可能造成:
-
内存快速上涨
-
页面卡顿
-
图片解码变慢
-
Android 低内存设备闪退
-
iOS 被系统终止
二十六、cacheWidth 和 cacheHeight
如果界面只需要显示 200 × 200 的头像,没有必要始终按原始 4000 × 3000 解码。
Image.network(
photoUrl,
width: 100,
height: 100,
fit: BoxFit.cover,
cacheWidth: 300,
cacheHeight: 300,
);
cacheWidth 和 cacheHeight 会让引擎按照指定的解码尺寸处理和缓存图片,而不是始终使用图片原始尺寸,这可以显著降低内存占用。Flutter 官方文档还给出了大尺寸图片缩小解码后可大幅减少内存的示例。
注意:
width / height
通常是逻辑像素
cacheWidth / cacheHeight
是图片解码像素尺寸
可以结合设备像素比例估算:
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
final cacheSize = (100 * pixelRatio).round();
Image.network(
photoUrl,
width: 100,
height: 100,
cacheWidth: cacheSize,
cacheHeight: cacheSize,
fit: BoxFit.cover,
);
不要盲目设置得过小,否则图片会模糊。
二十七、gaplessPlayback
当 ImageProvider 发生变化时,默认行为可能短暂地不显示旧图。
Image.network(
currentPhotoUrl,
gaplessPlayback: true,
);
设置为 true 后,新图片加载期间可以继续显示旧图片。官方 API 中默认值为 false。
适合:
-
用户切换照片
-
轮播图更新
-
图片 URL 刷新
-
避免旧图和新图之间短暂闪白
但需要谨慎:
如果旧图属于另一个用户,继续显示旧图可能让用户误以为数据已经更新。
二十八、图片无障碍说明
有实际信息意义的图片:
Image.network(
photoUrl,
semanticLabel: 'Profile photo of Alice',
);
纯装饰图片:
Image.asset(
'assets/images/background.png',
excludeFromSemantics: true,
);
避免屏幕阅读器朗读没有意义的装饰内容。
二十九、Placeholder
Placeholder 用于开发阶段表示一个尚未完成的区域。
const Placeholder();
它会显示一个带交叉线的方框。
当父组件提供有限空间时,它会适应父组件;处于无界空间时,则会使用 fallbackWidth 和 fallbackHeight。
使用示例
const SizedBox(
width: 200,
height: 120,
child: Placeholder(
color: Colors.red,
strokeWidth: 2,
),
);
适合:
-
页面结构尚未完成
-
图片区域等待设计稿
-
联调前临时占位
-
确认布局约束
不适合正式发布给用户。
正式环境应该使用:
-
默认头像
-
空状态图片
-
Loading
-
Skeleton
-
错误占位图
三十、常见错误与正确写法
错误一:Row 中的长文本没有 Expanded
错误示例
Row(
children: [
const Icon(Icons.location_on),
Text(longLocation),
],
);
问题表现
文字溢出。
正确写法
Row(
children: [
const Icon(Icons.location_on),
const SizedBox(width: 4),
Expanded(
child: Text(
longLocation,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
);
错误二:只设置 overflow,没有有限宽度
Text(
longText,
overflow: TextOverflow.ellipsis,
);
ellipsis 不负责创建宽度约束。
正确做法是确保父组件提供有限宽度。
错误三:使用 RichText 但没有根样式
RichText(
text: const TextSpan(
text: 'Hello',
),
);
可能出现样式不符合预期。
正确:
RichText(
text: TextSpan(
text: 'Hello',
style: DefaultTextStyle.of(context).style,
),
);
或者优先使用:
const Text.rich(
TextSpan(text: 'Hello'),
);
错误四:把 Icon 当成按钮
const Icon(Icons.close);
用户无法点击。
正确:
IconButton(
onPressed: closePage,
icon: const Icon(Icons.close),
);
错误五:头像使用 BoxFit。fill
Image.network(
photoUrl,
fit: BoxFit.fill,
);
人脸可能被拉伸。
正确:
Image.network(
photoUrl,
fit: BoxFit.cover,
);
错误六:设置圆角但没有裁剪图片
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
),
child: Image.network(photoUrl),
);
正确:
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
photoUrl,
fit: BoxFit.cover,
),
);
错误七:小头像解码超大原图
Image.network(
photoUrl,
width: 50,
height: 50,
);
显示区域小,不代表图片一定按 50 × 50 解码。
正确:
Image.network(
photoUrl,
width: 50,
height: 50,
cacheWidth: 150,
cacheHeight: 150,
fit: BoxFit.cover,
);
具体值应结合设备像素比例和图片清晰度需求。
三十一、完整可运行示例
import 'package:flutter/material.dart';
void main() {
runApp(const TextImageDemoApp());
}
class TextImageDemoApp extends StatelessWidget {
const TextImageDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Text and Image Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
),
useMaterial3: true,
),
home: const TextImageDemoPage(),
);
}
}
class TextImageDemoPage extends StatelessWidget {
const TextImageDemoPage({super.key});
static const String photoUrl =
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg';
@override
Widget build(BuildContext context) {
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
final avatarCacheSize = (88 * pixelRatio).round();
return Scaffold(
appBar: AppBar(
title: const Text('Text, Icon and Image'),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text(
'1. Text overflow',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
const Row(
children: [
Icon(Icons.location_on),
SizedBox(width: 8),
Expanded(
child: Text(
'Los Angeles, California, United States of America',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 24),
const Text(
'2. Rich text',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
const Text.rich(
TextSpan(
children: [
TextSpan(text: 'Alice is '),
TextSpan(
text: 'online',
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
TextSpan(text: ' now.'),
],
),
),
const SizedBox(height: 24),
const Text(
'3. Selectable text',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
const SelectableText(
'User ID: 93432015',
),
const SizedBox(height: 24),
const Text(
'4. Icon theme',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
const IconTheme(
data: IconThemeData(
color: Colors.deepPurple,
size: 32,
),
child: Row(
children: [
Icon(Icons.favorite),
SizedBox(width: 16),
Icon(Icons.message),
SizedBox(width: 16),
Icon(Icons.person),
],
),
),
const SizedBox(height: 24),
const Text(
'5. Network image',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(44),
child: Image.network(
photoUrl,
width: 88,
height: 88,
cacheWidth: avatarCacheSize,
cacheHeight: avatarCacheSize,
fit: BoxFit.cover,
loadingBuilder: (
BuildContext context,
Widget child,
ImageChunkEvent? progress,
) {
if (progress == null) {
return child;
}
return const SizedBox(
width: 88,
height: 88,
child: Center(
child: CircularProgressIndicator(),
),
);
},
errorBuilder: (
BuildContext context,
Object error,
StackTrace? stackTrace,
) {
return Container(
width: 88,
height: 88,
color: Colors.grey.shade200,
alignment: Alignment.center,
child: const Icon(
Icons.person,
size: 42,
color: Colors.grey,
),
);
},
),
),
),
const SizedBox(height: 24),
const Text(
'6. Placeholder',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
const SizedBox(
height: 120,
child: Placeholder(),
),
],
),
);
}
}
三十二、本章选择原则
需求
推荐组件
普通单一样式文字
Text
一段文字包含不同样式
Text.rich
更底层的富文本控制
RichText
描述富文本结构
TextSpan
单段文字允许复制
SelectableText
整片区域允许选择
SelectionArea
为子树统一设置文字样式
DefaultTextStyle
只显示图标
Icon
可点击图标
IconButton
本地资源图片
Image.asset
网络图片
Image.network
本地文件图片
Image.file
内存字节图片
Image.memory
开发中的临时区域
Placeholder
三十三、本章核心总结
-
Text 能否换行,首先取决于父组件提供的宽度约束。
-
TextOverflow.ellipsis需要有限的宽度才能正常工作。 -
Row 中的长文本通常需要放进 Expanded 或 Flexible。
-
Text 会继承最近的 DefaultTextStyle。
-
大部分富文本优先使用
Text.rich。 -
RichText 更适合需要底层控制的场景。
-
TextSpan 的子节点可以继承和覆盖父节点的样式。
-
可点击 TextSpan 的 GestureRecognizer 需要正确释放。
-
Icon 本身不能点击,可点击场景应使用 IconButton。
-
用户头像通常使用
BoxFit.cover。 -
Logo 或不能裁剪的图片通常使用
BoxFit.contain。 -
圆角装饰不会自动裁剪图片。
-
网络图片应该处理 Loading 和 Error 状态。
-
图片显示尺寸小,不代表图片解码内存一定小。
-
可以使用
cacheWidth和cacheHeight降低大图内存占用。