第3章:基础组件 —— 3.1 文本及样式

118 阅读7分钟

3.1 文本及样式

📚 章节概览

文本是应用中最基本的组件之一。Flutter 提供了强大的文本显示和样式系统,本章节将学习:

  • Text - 基础文本组件
  • TextStyle - 文本样式定义
  • TextSpan - 富文本实现
  • DefaultTextStyle - 默认样式继承
  • 字体配置 - 自定义字体的使用

🎯 核心知识点

1. Text 组件

Text 是 Flutter 中用于显示文本的基本组件。

基本用法
Text('Hello World')
常用属性
属性类型说明
dataString要显示的文本
styleTextStyle?文本样式
textAlignTextAlign?文本对齐方式
maxLinesint?最大行数
overflowTextOverflow?溢出处理
softWrapbool是否自动换行(默认true)
textScaleFactordouble?文本缩放因子
对齐方式(TextAlign)
TextAlign.left     // 左对齐
TextAlign.center   // 居中对齐
TextAlign.right    // 右对齐
TextAlign.justify  // 两端对齐
TextAlign.start    // 起始位置对齐(LTR时为左,RTL时为右)
TextAlign.end      // 结束位置对齐
溢出处理(TextOverflow)
TextOverflow.clip      // 裁剪溢出文本
TextOverflow.fade      // 淡出效果
TextOverflow.ellipsis  // 省略号(...)
TextOverflow.visible   // 显示溢出文本

2. TextStyle 文本样式

TextStyle 用于定义文本的外观。

完整属性列表
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)
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)
TextDecoration.none          // 无装饰
TextDecoration.underline     // 下划线
TextDecoration.overline      // 上划线
TextDecoration.lineThrough   // 删除线

3. TextSpan 富文本

TextSpan 用于在一段文本中使用不同的样式。

基本用法
Text.rich(
  TextSpan(
    text: '普通文本 ',
    style: TextStyle(fontSize: 16),
    children: [
      TextSpan(
        text: '粗体 ',
        style: TextStyle(fontWeight: FontWeight.bold),
      ),
      TextSpan(
        text: '蓝色',
        style: TextStyle(color: Colors.blue),
      ),
    ],
  ),
)
可点击的文本

使用 GestureRecognizer 实现文本点击:

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 组件的默认样式。

用法
DefaultTextStyle(
  style: TextStyle(
    color: Colors.red,
    fontSize: 20.0,
  ),
  child: Column(
    children: [
      Text('继承红色和20px'),
      Text('也继承默认样式'),
      Text(
        '不继承默认样式',
        style: TextStyle(
          inherit: false,  // 禁用继承
          color: Colors.blue,
        ),
      ),
    ],
  ),
)
样式继承规则
  1. 默认行为:子 Text 的 style 会与 DefaultTextStyle 合并
  2. 覆盖规则:子 Text 明确指定的属性会覆盖默认值
  3. 禁用继承:设置 inherit: false 完全不继承

5. 自定义字体

步骤1:添加字体文件

在项目根目录创建 fonts 文件夹,放入字体文件:

fonts/
  ├── Roboto-Regular.ttf
  ├── Roboto-Bold.ttf
  └── Roboto-Italic.ttf
步骤2:在 pubspec.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:使用字体
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

说明:

  1. Text 是一个便利的 Widget,内部使用 RichText
  2. RichText 使用 TextPainter 进行文本布局
  3. TextSpan 树描述文本的样式和结构
  4. 最终通过 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 定义全局文本样式

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. 提取常用样式为常量

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. 处理文本溢出

// ✅ 推荐:显式指定溢出处理
Text(
  longText,
  maxLines: 2,
  overflow: TextOverflow.ellipsis,
)

// ❌ 避免:不处理溢出可能导致布局问题
Text(longText)

4. 使用 textScaleFactor 支持无障碍

// Flutter 会自动应用系统字体缩放设置
// 确保文本在用户调整系统字体大小时能正常显示

Text(
  'Accessible Text',
  textScaleFactor: MediaQuery.of(context).textScaleFactor,
)

🤔 常见问题(FAQ)

Q1: Text 的宽度是如何确定的?

A: Text 的宽度取决于父组件的约束:

  • 无约束:宽度为文本内容宽度
  • 有最大宽度约束:不超过最大宽度,内容过长会换行
  • 强制宽度:Text 会占满该宽度
// 1. 内容宽度
Text('Hello')  // 宽度 = 文本宽度

// 2. 受约束
Container(
  width: 200,
  child: Text('Long text...'),  // 最大宽度200,自动换行
)

// 3. 强制宽度
SizedBox(
  width: 200,
  child: Text('Short'),  // 占满200宽度
)

Q2: height 属性是什么意思?

A: height行高倍数,不是像素值:

TextStyle(
  fontSize: 16,
  height: 1.5,  // 行高 = 16 * 1.5 = 24px
)
  • height: 1.0 - 行高等于字体大小(紧凑)
  • height: 1.5 - 常用的舒适行高
  • height: 2.0 - 较大的行间距

Q3: 如何实现"查看更多"功能?

A: 使用 TextPainter 检测文本是否溢出:

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:

特性TextTextSpan
用途单一样式文本富文本(多样式)
Widget?否(需要用 Text.rich 或 RichText)
嵌套不支持支持(children)
手势整体可以针对每个 span
// 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: 使用 ShaderMaskforeground 属性:

// 方法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:实现一个价格标签

目标: 显示原价(删除线)和现价(红色大字)

步骤:

  1. 使用 Text.richTextSpan
  2. 原价:灰色 + 删除线
  3. 现价:红色 + 大字号 + 粗体
💡 查看答案
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:实现用户协议文本

目标: "我已阅读并同意《用户协议》和《隐私政策》",其中协议名称可点击

步骤:

  1. 使用 Text.richTextSpan
  2. 普通文本:黑色
  3. 协议名称:蓝色 + 下划线 + 可点击
  4. 使用 TapGestureRecognizer 添加点击事件
💡 查看答案
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:实现一个聊天气泡

目标: 显示聊天消息,包括用户名、时间和内容,样式要清晰

步骤:

  1. 用户名:粗体、蓝色
  2. 时间:小字、灰色
  3. 消息内容:正常大小、黑色
  4. 使用 DefaultTextStyle 设置基础样式
💡 查看答案
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

记忆技巧

  1. Text 三要素:内容(data)、样式(style)、对齐(textAlign)
  2. TextStyle 记忆:颜色大小粗细(color, fontSize, fontWeight)→ 间距高度(letterSpacing, height)→ 装饰阴影(decoration, shadows)
  3. 富文本:用 TextSpan 嵌套 → 用 recognizer 交互
  4. 样式继承DefaultTextStyle 包裹 → 子组件自动继承 → inherit: false 禁用

🔗 相关资源


下一节: 3.2 按钮

上一节: 2.8 Flutter异常捕获