3.1 文本及样式
📚 章节概览
文本是应用中最基本的组件之一。Flutter 提供了强大的文本显示和样式系统,本章节将学习:
- Text - 基础文本组件
- TextStyle - 文本样式定义
- TextSpan - 富文本实现
- DefaultTextStyle - 默认样式继承
- 字体配置 - 自定义字体的使用
🎯 核心知识点
1. Text 组件
Text 是 Flutter 中用于显示文本的基本组件。
基本用法
dart
Text('Hello World')
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
data |
String | 要显示的文本 |
style |
TextStyle? | 文本样式 |
textAlign |
TextAlign? | 文本对齐方式 |
maxLines |
int? | 最大行数 |
overflow |
TextOverflow? | 溢出处理 |
softWrap |
bool | 是否自动换行(默认true) |
textScaleFactor |
double? | 文本缩放因子 |
对齐方式(TextAlign)
dart
TextAlign.left // 左对齐
TextAlign.center // 居中对齐
TextAlign.right // 右对齐
TextAlign.justify // 两端对齐
TextAlign.start // 起始位置对齐(LTR时为左,RTL时为右)
TextAlign.end // 结束位置对齐
溢出处理(TextOverflow)
dart
TextOverflow.clip // 裁剪溢出文本
TextOverflow.fade // 淡出效果
TextOverflow.ellipsis // 省略号(...)
TextOverflow.visible // 显示溢出文本
2. TextStyle 文本样式
TextStyle 用于定义文本的外观。
完整属性列表
dart
TextStyle(
color: Colors.blue, // 文本颜色
fontSize: 18.0, // 字体大小
fontWeight: FontWeight.bold, // 字体粗细
fontStyle: FontStyle.italic, // 字体风格(正常/斜体)
letterSpacing: 2.0, // 字母间距
wordSpacing: 5.0, // 单词间距
height: 1.5, // 行高(相对于fontSize)
decoration: TextDecoration.underline, // 文本装饰
decorationColor: Colors.red, // 装饰颜色
decorationStyle: TextDecorationStyle.dashed, // 装饰样式
shadows: [ // 阴影
Shadow(
offset: Offset(2, 2),
blurRadius: 3,
color: Colors.grey,
),
],
fontFamily: 'Courier', // 字体家族
backgroundColor: Colors.yellow, // 背景色
)
字体粗细(FontWeight)
dart
FontWeight.w100 // 最细
FontWeight.w200
FontWeight.w300 // Light
FontWeight.w400 // Normal / Regular(默认)
FontWeight.w500 // Medium
FontWeight.w600 // Semi-bold
FontWeight.w700 // Bold
FontWeight.w800
FontWeight.w900 // 最粗
文本装饰(TextDecoration)
dart
TextDecoration.none // 无装饰
TextDecoration.underline // 下划线
TextDecoration.overline // 上划线
TextDecoration.lineThrough // 删除线
3. TextSpan 富文本
TextSpan 用于在一段文本中使用不同的样式。
基本用法
dart
Text.rich(
TextSpan(
text: '普通文本 ',
style: TextStyle(fontSize: 16),
children: [
TextSpan(
text: '粗体 ',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(
text: '蓝色',
style: TextStyle(color: Colors.blue),
),
],
),
)
可点击的文本
使用 GestureRecognizer 实现文本点击:
dart
import 'package:flutter/gestures.dart';
Text.rich(
TextSpan(
text: '点击 ',
children: [
TextSpan(
text: '这里',
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
print('链接被点击');
},
),
],
),
)
4. DefaultTextStyle 默认样式
DefaultTextStyle 用于设置子树中所有 Text 组件的默认样式。
用法
dart
DefaultTextStyle(
style: TextStyle(
color: Colors.red,
fontSize: 20.0,
),
child: Column(
children: [
Text('继承红色和20px'),
Text('也继承默认样式'),
Text(
'不继承默认样式',
style: TextStyle(
inherit: false, // 禁用继承
color: Colors.blue,
),
),
],
),
)
样式继承规则
- 默认行为 :子 Text 的
style会与DefaultTextStyle合并 - 覆盖规则:子 Text 明确指定的属性会覆盖默认值
- 禁用继承 :设置
inherit: false完全不继承
5. 自定义字体
步骤1:添加字体文件
在项目根目录创建 fonts 文件夹,放入字体文件:
scss
fonts/
├── Roboto-Regular.ttf
├── Roboto-Bold.ttf
└── Roboto-Italic.ttf
步骤2:在 pubspec.yaml 中声明
yaml
flutter:
fonts:
- family: Roboto
fonts:
- asset: fonts/Roboto-Regular.ttf
- asset: fonts/Roboto-Bold.ttf
weight: 700
- asset: fonts/Roboto-Italic.ttf
style: italic
步骤3:使用字体
dart
Text(
'Custom Font',
style: TextStyle(
fontFamily: 'Roboto',
fontSize: 20,
),
)
🔍 工作原理
Text 的渲染流程
flowchart TB
A["Text Widget"] --> B["RichText Widget"]
B --> C["TextPainter"]
C --> D["TextSpan 树"]
D --> E["Paragraph (Skia)"]
E --> F["Canvas 绘制"]
style A fill:#e1f5ff
style B fill:#e1f5ff
style C fill:#fff9e1
style D fill:#fff9e1
style E fill:#ffe1f5
style F fill:#ffe1f5
说明:
Text是一个便利的 Widget,内部使用RichTextRichText使用TextPainter进行文本布局TextSpan树描述文本的样式和结构- 最终通过 Skia 引擎的
Paragraph渲染
样式继承机制
flowchart TB
A["DefaultTextStyle"] --> B["合并样式"]
C["Text.style"] --> B
B --> D["最终样式"]
D --> E["应用到文本"]
style A fill:#e1f5ff
style C fill:#e1f5ff
style B fill:#fff9e1
style D fill:#ffe1f5
style E fill:#ffe1f5
合并规则:
- Text 明确指定的属性优先
- 未指定的属性从 DefaultTextStyle 继承
inherit: false则完全不继承
💡 最佳实践
1. 使用 Theme 定义全局文本样式
dart
MaterialApp(
theme: ThemeData(
textTheme: TextTheme(
displayLarge: TextStyle(fontSize: 96, fontWeight: FontWeight.w300),
displayMedium: TextStyle(fontSize: 60, fontWeight: FontWeight.w400),
bodyLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
bodyMedium: TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
),
),
)
// 使用
Text(
'Title',
style: Theme.of(context).textTheme.displayLarge,
)
2. 提取常用样式为常量
dart
class AppTextStyles {
static const title = TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
);
static const subtitle = TextStyle(
fontSize: 18,
color: Colors.grey,
);
static const body = TextStyle(
fontSize: 14,
height: 1.5,
);
}
// 使用
Text('Title', style: AppTextStyles.title)
3. 处理文本溢出
dart
// ✅ 推荐:显式指定溢出处理
Text(
longText,
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
// ❌ 避免:不处理溢出可能导致布局问题
Text(longText)
4. 使用 textScaleFactor 支持无障碍
dart
// Flutter 会自动应用系统字体缩放设置
// 确保文本在用户调整系统字体大小时能正常显示
Text(
'Accessible Text',
textScaleFactor: MediaQuery.of(context).textScaleFactor,
)
🤔 常见问题(FAQ)
Q1: Text 的宽度是如何确定的?
A: Text 的宽度取决于父组件的约束:
- 无约束:宽度为文本内容宽度
- 有最大宽度约束:不超过最大宽度,内容过长会换行
- 强制宽度:Text 会占满该宽度
dart
// 1. 内容宽度
Text('Hello') // 宽度 = 文本宽度
// 2. 受约束
Container(
width: 200,
child: Text('Long text...'), // 最大宽度200,自动换行
)
// 3. 强制宽度
SizedBox(
width: 200,
child: Text('Short'), // 占满200宽度
)
Q2: height 属性是什么意思?
A: height 是行高倍数,不是像素值:
dart
TextStyle(
fontSize: 16,
height: 1.5, // 行高 = 16 * 1.5 = 24px
)
height: 1.0- 行高等于字体大小(紧凑)height: 1.5- 常用的舒适行高height: 2.0- 较大的行间距
Q3: 如何实现"查看更多"功能?
A: 使用 TextPainter 检测文本是否溢出:
dart
class ExpandableText extends StatefulWidget {
final String text;
final int maxLines;
const ExpandableText({
super.key,
required this.text,
this.maxLines = 3,
});
@override
State<ExpandableText> createState() => _ExpandableTextState();
}
class _ExpandableTextState extends State<ExpandableText> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.text,
maxLines: _expanded ? null : widget.maxLines,
overflow: _expanded ? null : TextOverflow.ellipsis,
),
GestureDetector(
onTap: () {
setState(() {
_expanded = !_expanded;
});
},
child: Text(
_expanded ? '收起' : '查看更多',
style: TextStyle(color: Colors.blue),
),
),
],
);
}
}
Q4: TextSpan 和 Text 有什么区别?
A:
| 特性 | Text | TextSpan |
|---|---|---|
| 用途 | 单一样式文本 | 富文本(多样式) |
| Widget? | 是 | 否(需要用 Text.rich 或 RichText) |
| 嵌套 | 不支持 | 支持(children) |
| 手势 | 整体 | 可以针对每个 span |
dart
// Text:单一样式
Text(
'Simple Text',
style: TextStyle(color: Colors.blue),
)
// TextSpan:多样式
Text.rich(
TextSpan(
children: [
TextSpan(text: 'Hello ', style: TextStyle(color: Colors.black)),
TextSpan(text: 'World', style: TextStyle(color: Colors.blue)),
],
),
)
Q5: 如何实现渐变色文本?
A: 使用 ShaderMask 或 foreground 属性:
dart
// 方法1:ShaderMask
ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [Colors.blue, Colors.purple],
).createShader(bounds),
child: Text(
'Gradient Text',
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.white, // 必须设置为白色
),
),
)
// 方法2:foreground(Paint)
Text(
'Gradient Text',
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
foreground: Paint()
..shader = LinearGradient(
colors: [Colors.blue, Colors.purple],
).createShader(Rect.fromLTWH(0, 0, 200, 70)),
),
)
🎯 跟着做练习
练习1:实现一个价格标签
目标: 显示原价(删除线)和现价(红色大字)
步骤:
- 使用
Text.rich和TextSpan - 原价:灰色 + 删除线
- 现价:红色 + 大字号 + 粗体
💡 查看答案
dart
class PriceTag extends StatelessWidget {
final double originalPrice;
final double currentPrice;
const PriceTag({
super.key,
required this.originalPrice,
required this.currentPrice,
});
@override
Widget build(BuildContext context) {
return Text.rich(
TextSpan(
children: [
const TextSpan(
text: '原价:',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
TextSpan(
text: '¥${originalPrice.toStringAsFixed(2)} ',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
decoration: TextDecoration.lineThrough,
),
),
const TextSpan(
text: '现价:',
style: TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
TextSpan(
text: '¥${currentPrice.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 24,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}
// 使用
PriceTag(originalPrice: 299.0, currentPrice: 199.0)
练习2:实现用户协议文本
目标: "我已阅读并同意《用户协议》和《隐私政策》",其中协议名称可点击
步骤:
- 使用
Text.rich和TextSpan - 普通文本:黑色
- 协议名称:蓝色 + 下划线 + 可点击
- 使用
TapGestureRecognizer添加点击事件
💡 查看答案
dart
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class AgreementText extends StatelessWidget {
final VoidCallback? onUserAgreementTap;
final VoidCallback? onPrivacyPolicyTap;
const AgreementText({
super.key,
this.onUserAgreementTap,
this.onPrivacyPolicyTap,
});
@override
Widget build(BuildContext context) {
return Text.rich(
TextSpan(
style: const TextStyle(fontSize: 12, color: Colors.black87),
children: [
const TextSpan(text: '我已阅读并同意'),
TextSpan(
text: '《用户协议》',
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
onUserAgreementTap?.call();
},
),
const TextSpan(text: '和'),
TextSpan(
text: '《隐私政策》',
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
onPrivacyPolicyTap?.call();
},
),
],
),
);
}
}
// 使用
AgreementText(
onUserAgreementTap: () {
print('查看用户协议');
},
onPrivacyPolicyTap: () {
print('查看隐私政策');
},
)
练习3:实现一个聊天气泡
目标: 显示聊天消息,包括用户名、时间和内容,样式要清晰
步骤:
- 用户名:粗体、蓝色
- 时间:小字、灰色
- 消息内容:正常大小、黑色
- 使用
DefaultTextStyle设置基础样式
💡 查看答案
dart
class ChatBubble extends StatelessWidget {
final String userName;
final String message;
final DateTime time;
final bool isMe;
const ChatBubble({
super.key,
required this.userName,
required this.message,
required this.time,
this.isMe = false,
});
String _formatTime(DateTime dateTime) {
return '${dateTime.hour.toString().padLeft(2, '0')}:'
'${dateTime.minute.toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
return Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isMe ? Colors.blue.withValues(alpha: 0.1) : Colors.grey.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 用户名和时间
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
userName,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: isMe ? Colors.blue : Colors.green,
),
),
const SizedBox(width: 8),
Text(
_formatTime(time),
style: const TextStyle(
fontSize: 10,
color: Colors.grey,
),
),
],
),
const SizedBox(height: 4),
// 消息内容
Text(
message,
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
height: 1.4,
),
),
],
),
),
);
}
}
// 使用示例
class ChatBubbleDemo extends StatelessWidget {
const ChatBubbleDemo({super.key});
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(8),
children: [
ChatBubble(
userName: '张三',
message: '你好!今天天气真不错。',
time: DateTime.now().subtract(const Duration(minutes: 5)),
isMe: false,
),
ChatBubble(
userName: '我',
message: '是啊,要不要出去走走?',
time: DateTime.now().subtract(const Duration(minutes: 3)),
isMe: true,
),
ChatBubble(
userName: '张三',
message: '好主意!几点出发?',
time: DateTime.now().subtract(const Duration(minutes: 1)),
isMe: false,
),
],
);
}
}
📋 小结
核心要点
| 组件 | 用途 | 关键属性 |
|---|---|---|
| Text | 显示文本 | style, textAlign, maxLines, overflow |
| TextStyle | 定义样式 | color, fontSize, fontWeight, height |
| TextSpan | 富文本 | children, recognizer |
| DefaultTextStyle | 默认样式 | style, inherit |
记忆技巧
- Text 三要素:内容(data)、样式(style)、对齐(textAlign)
- TextStyle 记忆:颜色大小粗细(color, fontSize, fontWeight)→ 间距高度(letterSpacing, height)→ 装饰阴影(decoration, shadows)
- 富文本 :用
TextSpan嵌套 → 用recognizer交互 - 样式继承 :
DefaultTextStyle包裹 → 子组件自动继承 →inherit: false禁用
🔗 相关资源
- Flutter Text 官方文档
- TextStyle 官方文档
- Google Fonts 包 - 快速使用 Google 字体
- 自定义字体指南
下一节: 3.2 按钮
上一节: 2.8 Flutter异常捕获