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

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,
        ),
      ),
    ],
  ),
)
样式继承规则
  1. 默认行为 :子 Text 的 style 会与 DefaultTextStyle 合并
  2. 覆盖规则:子 Text 明确指定的属性会覆盖默认值
  3. 禁用继承 :设置 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

说明:

  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 定义全局文本样式

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: 使用 ShaderMaskforeground 属性:

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

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

步骤:

  1. 使用 Text.richTextSpan
  2. 原价:灰色 + 删除线
  3. 现价:红色 + 大字号 + 粗体

💡 查看答案

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:实现用户协议文本

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

步骤:

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

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

步骤:

  1. 用户名:粗体、蓝色
  2. 时间:小字、灰色
  3. 消息内容:正常大小、黑色
  4. 使用 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

记忆技巧

  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异常捕获

相关推荐
旧时光_2 小时前
第2章:第一个Flutter应用 —— 2.8 Flutter异常捕获
flutter
猪哥帅过吴彦祖3 小时前
Flutter 系列教程:应用导航 - Navigator 1.0 与命名路由
android·flutter·ios
旧时光_3 小时前
第2章:第一个Flutter应用 —— 2.7 调试Flutter应用
flutter
鹏多多4 小时前
flutter图片选择库multi_image_picker_plus和image_picker的对比和使用解析
android·flutter·ios
GISer_Jing5 小时前
Flutter架构解析:从引擎层到应用层
前端·flutter·架构
lqj_本人5 小时前
Flutter与鸿蒙EventChannel事件流通信详解
flutter
lpfasd1235 小时前
Flutter持续健康发展的多维度分析
flutter
GISer_Jing5 小时前
Flutter开发全攻略:从入门到精通
android·前端·flutter