149 lines
4.9 KiB
Markdown
149 lines
4.9 KiB
Markdown
# 【Demo】Text 文本组件全家桶
|
||
|
||
> 源文件:`lib/pages/demo/text_demo.dart` | 分类:基础布局
|
||
> 一句话:样式、对齐、截断、行高、富文本、可点击协议文本 —— 文本相关的细节一次看全。
|
||
|
||
---
|
||
|
||
## 一、这个 Demo 解决什么问题
|
||
|
||
Text 是 Flutter 里出现频率最高的组件,但细节坑不少:
|
||
- `textAlign` 为什么有时不生效?
|
||
- 超长文字怎么显示省略号?
|
||
- "已阅读并同意《用户协议》"这种**部分文字可点击**怎么做?
|
||
- 系统字体缩放怎么适配?
|
||
|
||
本页 7 个区块逐一演示。
|
||
|
||
---
|
||
|
||
## 二、怎么运行 / 演示内容
|
||
|
||
```dart
|
||
runApp(const MaterialApp(home: TextDemoPage()));
|
||
```
|
||
|
||
1. 基础样式(fontSize / color / fontWeight / fontStyle);
|
||
2. 对齐 textAlign(左/中/右);
|
||
3. 截断三件套(ellipsis / clip / softWrap:false);
|
||
4. 行高 height 对比(默认 / 1.2 / 2);
|
||
5. 字间距 letterSpacing / 词间距 wordSpacing;
|
||
6. 富文本 Text.rich + TextSpan + 可点击协议;
|
||
7. 字体缩放 textScaler。
|
||
|
||
---
|
||
|
||
## 三、核心知识点(新手向)
|
||
|
||
### 1. Text 全家桶速记
|
||
| 场景 | 组件 |
|
||
|---|---|
|
||
| 单样式文字 | `Text` |
|
||
| 富文本拼接(不同颜色/可点击段落) | `Text.rich` + `TextSpan` |
|
||
| 全局默认样式 | `DefaultTextStyle`(MaterialApp 自带,Text 会继承) |
|
||
|
||
### 2. textAlign 生效的前提
|
||
**Text 必须有宽度约束**,对齐才有空间生效:
|
||
```dart
|
||
Container(
|
||
width: 300.w,
|
||
child: Column(
|
||
// 关键:stretch 让每个子组件横向拉满,textAlign 才有空间生效
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Text('左对齐', textAlign: TextAlign.left),
|
||
Text('居中对齐', textAlign: TextAlign.center),
|
||
Text('右对齐', textAlign: TextAlign.right),
|
||
],
|
||
),
|
||
)
|
||
```
|
||
> 新手最常见的疑惑:在 Column 里写 `textAlign: center` 不居中 —— 因为 Text 宽度只包住文字。
|
||
> 要么 `crossAxisAlignment: center`(让 Text 居中),要么 `stretch` + `textAlign`(让文字在拉满的宽度里居中)。
|
||
|
||
### 3. 截断三件套(必须配齐)
|
||
```dart
|
||
Text(
|
||
'很长很长...',
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis, // 末尾加省略号
|
||
)
|
||
```
|
||
- `ellipsis`:省略号;`clip`:直接裁掉;`softWrap: false` + `visible`:真·溢出画到盒子外;
|
||
- 前提同样是**有界宽度**(外面套固定宽 Container)。
|
||
|
||
### 4. 行高与间距
|
||
```dart
|
||
TextStyle(height: 1.2) // 行高倍数(1.0 = 紧凑,2.0 = 松散)
|
||
TextStyle(letterSpacing: 4) // 字间距(中文按字)
|
||
TextStyle(wordSpacing: 8) // 词间距(按空格分词,中文基本无效)
|
||
```
|
||
|
||
### 5. 富文本 + 可点击(协议文本的官方姿势)
|
||
```dart
|
||
Text.rich(
|
||
TextSpan(
|
||
children: [
|
||
TextSpan(text: '已阅读并同意', style: TextStyle(color: Color(0xFF888888))),
|
||
TextSpan(
|
||
text: '《用户协议》',
|
||
style: TextStyle(color: Color(0xFF3B82F6), fontWeight: FontWeight.bold),
|
||
recognizer: _agreementRecognizer, // 点击手势(必须 dispose!)
|
||
),
|
||
],
|
||
),
|
||
)
|
||
```
|
||
`TapGestureRecognizer` 必须放在 **StatefulWidget 里创建,并在 dispose() 里释放**(与 createState 一一对应,防内存泄漏)。
|
||
|
||
### 6. 字体缩放(Flutter 3.x)
|
||
```dart
|
||
Text('线性放大 1.5 倍', textScaler: TextScaler.linear(1.5));
|
||
// 读取系统缩放:MediaQuery.textScalerOf(context)
|
||
```
|
||
|
||
---
|
||
|
||
## 四、关键代码剖析
|
||
|
||
```dart
|
||
class _AgreementRichTextState extends State<_AgreementRichText> {
|
||
// late final:整个 State 生命周期只创建一次,复用同一个 recognizer
|
||
late final TapGestureRecognizer _agreementRecognizer = TapGestureRecognizer()
|
||
..onTap = _handleAgreementTap;
|
||
|
||
void _handleAgreementTap() {
|
||
// 点击回调可能晚于 dispose(点击瞬间页面被切走),用 mounted 兜底
|
||
if (!mounted) return;
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('点了《用户协议》')),
|
||
);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_agreementRecognizer.dispose(); // 关键:释放 recognizer
|
||
super.dispose();
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 五、踩坑记录
|
||
|
||
1. **`textScaleFactor` 已废弃**(Flutter 3.x)→ 用 `textScaler` / `MediaQuery.textScalerOf(context)`;
|
||
2. **`withOpacity` 已废弃** → 用 `withValues(alpha: x)`;
|
||
3. textAlign 不生效 = 没有宽度约束(用 stretch 或固定宽);
|
||
4. 截断不生效 = 没有 `maxLines`(三件套要配齐);
|
||
5. `TapGestureRecognizer` 不 dispose 会内存泄漏(页面反复进出后崩溃/卡顿)。
|
||
|
||
---
|
||
|
||
## 六、扩展思考
|
||
|
||
- 富文本进阶:本项目的 `RichTextContent`([rich_text_demo.md](rich_text_demo.md))渲染 HTML 富文本(题目/解析),
|
||
底层就是 TextSpan 体系的自动生成;
|
||
- 全局字体缩放:`MaterialApp(builder: ...)` 里统一包 `MediaQuery` 改造,实现"阅读字号调节"(见 rich_text_demo 的 Aa±);
|
||
- 文本选择:`SelectableText` 让用户可复制(本项目的日志区就用它)。
|