Flutter for OpenHarmony:构建一个 Flutter ASCII 艺术生成器,深入解析字符映射、动态文本渲染与等宽字体实践

发布时间:2026年1月28日
技术栈:Flutter 3.22+、Dart 3.4+、Material Design 3
适用读者:熟悉 Flutter 基础,希望掌握字符处理、动态文本布局、剪贴板集成及等宽字体应用的开发者


在数字艺术的长河中,ASCII 艺术(ASCII Art)是一种用可打印字符(如 , , 等)构建图像的独特表达形式。它诞生于早期终端时代,却因其极简、怀旧与创意性,在今日仍被广泛用于签名、聊天、代码注释甚至 NFT 创作。
在这里插入图片描述
全文代码

```dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const AsciiArtApp());
}

class AsciiArtApp extends StatelessWidget {
  const AsciiArtApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '🔤 ASCII 艺术',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(useMaterial3: true),
      home: const AsciiArtScreen(),
    );
  }
}

class AsciiArtScreen extends StatefulWidget {
  const AsciiArtScreen({super.key});

  @override
  State createState() => _AsciiArtScreenState();
}

class _AsciiArtScreenState extends State {
  final TextEditingController _textController = TextEditingController(text: 'HELLO');
  String _asciiOutput = '';

  // ASCII 字体数据(block style,5行高)
  static const Map> _fontMap = {
    'A': [' ██ ', '███ ', '████', '██ █', '██ █'],
    'B': ['███ ', '██ █', '███ ', '██ █', '███ '],
    'C': [' ██ ', '██  ', '██  ', '██  ', ' ██ '],
    'D': ['███ ', '██ █', '██ █', '██ █', '███ '],
    'E': ['████', '██  ', '███ ', '██  ', '████'],
    'F': ['████', '██  ', '███ ', '██  ', '██  '],
    'G': [' ██ ', '██  ', '██ █', '██ █', ' ███'],
    'H': ['██ █', '██ █', '████', '██ █', '██ █'],
    'I': ['███', ' █ ', ' █ ', ' █ ', '███'],
    'J': ['  ██', '  ██', '  ██', '███ ', '██  '],
    'K': ['██ █', '███ ', '██  ', '███ ', '██ █'],
    'L': ['██  ', '██  ', '██  ', '██  ', '████'],
    'M': ['█  █', '██ ██', '█ █ █', '█   █', '█   █'],
    'N': ['██ █', '███ ', '████', '█ ██', '█ ██'],
    'O': [' ██ ', '██ █', '██ █', '██ █', ' ██ '],
    'P': ['███ ', '██ █', '███ ', '██  ', '██  '],
    'Q': [' ██ ', '██ █', '██ █', '██ █', ' ███'],
    'R': ['███ ', '██ █', '███ ', '██ █', '██ █'],
    'S': [' ██ ', '██  ', ' ██ ', '  ██', '██ '],
    'T': ['████', ' █  ', ' █  ', ' █  ', ' █  '],
    'U': ['██ █', '██ █', '██ █', '██ █', ' ███'],
    'V': ['██ █', '██ █', '██ █', ' █ █', ' █ █'],
    'W': ['█   █', '█   █', '█ █ █', '██ ██', '█   █'],
    'X': ['█   █', ' █ █ ', '  █  ', ' █ █ ', '█   █'],
    'Y': ['█   █', ' █ █ ', '  █  ', '  █  ', '  █  '],
    'Z': ['████', '   █', '  █ ', ' █  ', '████'],
    '0': [' ██ ', '█  █', '█ ██', '██ █', ' ██ '],
    '1': [' █ ', '██ ', ' █ ', ' █ ', '███'],
    '2': [' ██ ', '   █', ' ██ ', '█   ', '████'],
    '3': ['███ ', '   █', ' ██ ', '   █', '███ '],
    '4': ['  █ ', ' ██ ', '████', '  █ ', '  █ '],
    '5': ['████', '█   ', '███ ', '   █', '███ '],
    '6': [' ██ ', '█   ', '███ ', '█ ██', ' ██ '],
    '7': ['████', '   █', '  █ ', ' █  ', '█   '],
    '8': [' ██ ', '█ ██', ' ██ ', '██ █', ' ██ '],
    '9': [' ██ ', '█ ██', ' ███', '   █', ' ██ '],
    '?': [' ██ ', '   █', '  █ ', '     ', '  █  '],
    ' ': ['    ', '    ', '    ', '    ', '    '],
  };

  String _generateAsciiArt(String input) {
    if (input.isEmpty) return '';
    final normalized = input.toUpperCase();
    final lines = List.generate(5, (_) => '');

    for (int i = 0; i < normalized.length; i++) {
      final char = normalized[i];
      final charLines = _fontMap[char] ?? _fontMap['?']!;
      for (int lineIndex = 0; lineIndex < 5; lineIndex++) {
        lines[lineIndex] = '${lines[lineIndex]}${charLines[lineIndex]} ';
      }
    }

    return lines.join('\n');
  }

  void _updateAscii() {
    final text = _textController.text;
    final output = _generateAsciiArt(text);
    setState(() {
      _asciiOutput = output;
    });
  }

  Future _copyToClipboard() async {
    if (_asciiOutput.isEmpty) return;
    await Clipboard.setData(ClipboardData(text: _asciiOutput));
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('已复制 ASCII 艺术到剪贴板 ✅')),
    );
  }

  @override
  void initState() {
    super.initState();
    _updateAscii();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('🔤 ASCII 艺术生成器'),
        centerTitle: true,
        backgroundColor: Theme.of(context).colorScheme.primaryContainer,
        actions: [
          IconButton(
            icon: const Icon(Icons.copy),
            onPressed: _copyToClipboard,
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: _textController,
              decoration: const InputDecoration(
                labelText: '输入文字(英文/数字)',
                border: OutlineInputBorder(),
                hintText: '例如:FLUTTER',
              ),
              onChanged: (_) => _updateAscii(),
            ),
            const SizedBox(height: 20),
            const Text(
              '点击右上角复制按钮分享你的 ASCII 艺术!',
              style: TextStyle(color: Colors.grey, fontSize: 14),
            ),
            const SizedBox(height: 20),
            Expanded(
              child: SingleChildScrollView(
                scrollDirection: Axis.vertical,
                child: SelectableText(
                  _asciiOutput,
                  style: const TextStyle(
                    fontFamily: 'monospace',
                    fontSize: 16,
                    height: 1.2,
                  ),
                  textAlign: TextAlign.center,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```

今天,我们将深入剖析一个用 Flutter 实现的 实时 ASCII 艺术生成器,重点探讨其如何通过 静态字符映射表多行文本拼接算法等宽字体渲染 以及 可选择文本交互,打造一个轻量但功能完整的创意工具。


🔤 功能需求与核心挑战

我们的 ASCII 艺术生成器需满足以下体验目标:

  • 实时预览:用户输入时立即生成对应艺术字
  • 支持英文与数字:覆盖 A–Z、0–9 及空格
  • 固定高度字体:确保字符对齐不偏移
  • 一键复制:将生成结果复制到剪贴板
  • 响应式布局:适配手机竖屏与横屏
  • 纯 Dart 实现:无网络请求、无外部依赖

这些需求背后隐藏着几个关键技术决策点:

  • 如何高效表示每个字符的“像素”结构?
  • 如何将多个字符的多行数据横向拼接成完整图案?
  • 如何确保在不同设备上字符严格对齐?

接下来,我们将逐层拆解。


🧠 数据模型:静态字符映射表设计

字体定义:5 行高 Block 风格

static const Map<String, List<String>> _fontMap = {
  'A': [' ██ ', '███ ', '████', '██ █', '██ █'],
  'B': ['███ ', '██ █', '███ ', '██ █', '███ '],
  // ...
  ' ': ['    ', '    ', '    ', '    ', '    '],
};

在这里插入图片描述

设计亮点

  1. static const 编译期常量

    • 内存占用低,启动快
    • 无运行时解析开销
  2. 每字符 5 行 × 4 列

    • 固定高度保证垂直对齐
    • 宽度统一(含尾部空格)便于横向拼接
  3. 使用空格 + 全角块字符

    • (U+2588 FULL BLOCK)作为“像素”
    • 空格作为背景,形成黑白对比
  4. 默认兜底字符 ?

    final charLines = _fontMap[char] ?? _fontMap['?']!;
    
    • 输入非支持字符(如中文、符号)时显示问号图案
    • 避免程序崩溃或空白

💡 为何不用矢量或图片?
ASCII 艺术的本质是“文本”,必须保持纯字符属性,才能被复制、粘贴、嵌入代码或终端。


🧩 核心算法:多字符横向拼接

输入 → 输出转换逻辑

String _generateAsciiArt(String input) {
  if (input.isEmpty) return '';
  final normalized = input.toUpperCase();
  final lines = List.generate(5, (_) => '');

  for (int i = 0; i < normalized.length; i++) {
    final char = normalized[i];
    final charLines = _fontMap[char] ?? _fontMap['?']!;
    for (int lineIndex = 0; lineIndex < 5; lineIndex++) {
      lines[lineIndex] = '${lines[lineIndex]}${charLines[lineIndex]} ';
    }
  }

  return lines.join('\n');
}

在这里插入图片描述

算法步骤解析

  1. 归一化toUpperCase() 统一大小写(因只定义大写)
  2. 初始化 5 行空字符串List.generate(5, (_) => '')
  3. 逐字符遍历
    • 获取该字符的 5 行图案
    • 逐行追加:将第 lineIndex 行内容拼接到 lines[lineIndex]
    • 添加列间距:每字符后加一个空格 ' ',避免粘连
  4. 合并为单字符串lines.join('\n') 生成最终多行文本

示例:输入 "AB"

A 的行 B 的行 拼接结果
0 ' ██ ' '███ ' ' ██ ███ '
1 '███ ' '██ █' '███ ██ █'

关键技巧先按行组织,再横向扩展,而非先按字符再纵向堆叠——这是实现对齐的核心。


🖼️ UI 渲染:等宽字体与可选择文本

为什么必须用等宽字体?

ASCII 艺术依赖 每个字符占据相同宽度,否则会出现错位。例如:

  • 非等宽字体(如默认系统字体):iW 宽度不同 → 图案扭曲
  • 等宽字体(monospace):所有字符等宽 → 图案精准对齐
SelectableText(
  _asciiOutput,
  style: const TextStyle(
    fontFamily: 'monospace', // 关键!
    fontSize: 16,
    height: 1.2,
  ),
)

使用 SelectableText 而非 Text

  • 允许用户长按选择文本:方便手动复制或分享
  • 保留原始换行与空格Text 可能压缩空白,SelectableText 严格保留
  • 无障碍友好:支持 TalkBack 朗读(尽管是乱码 😅)

布局优化

  • Expanded + SingleChildScrollView
    • 确保长文本可滚动
    • 不挤压上方输入框
  • textAlign: TextAlign.center:居中显示,视觉更平衡

📋 剪贴板集成:安全复制与用户反馈

Future<void> _copyToClipboard() async {
  if (_asciiOutput.isEmpty) return;
  await Clipboard.setData(ClipboardData(text: _asciiOutput));
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('已复制 ASCII 艺术到剪贴板 ✅')),
  );
}

在这里插入图片描述

关键细节

  1. 空值保护if (_asciiOutput.isEmpty) return;
  2. mounted 检查:防止异步回调时页面已销毁
  3. 即时反馈SnackBar 确认操作成功

📱 平台行为:iOS 首次访问剪贴板会弹出权限提示(系统级,无法绕过)


⌨️ 交互设计:实时更新与输入引导

实时响应输入

TextField(
  onChanged: (_) => _updateAscii(),
)

在这里插入图片描述

  • 无需提交按钮:输入即更新,提升流畅度
  • 性能安全:算法 O(n),即使长文本也毫秒级完成

输入提示

  • hintText: '例如:FLUTTER':引导用户输入有效内容
  • labelText:明确说明支持范围(英文/数字)

🎨 视觉与体验细节

1. AppBar 集成复制按钮

  • 右上角 IconButton 符合 Material Design 操作惯例
  • 图标直观(Icons.copy

2. 色彩与排版

  • 使用 Theme.of(context).colorScheme.primaryContainer 作为 AppBar 背景
  • 灰色提示文字降低干扰

3. 空状态处理

  • 初始显示 "HELLO",避免空白尴尬
  • 即使清空输入,也会生成空格图案(因 _fontMap[' '] 存在)

🚀 扩展方向:从工具到创作平台

当前实现可轻松升级为更强大的 ASCII 创作工具:

1. 多字体支持

  • 添加 slantsmallbanner 等风格
  • 下拉菜单切换字体

2. 自定义字符集

  • 允许用户上传或绘制自己的 5×5 字符
  • 保存为本地模板

3. 导出为图片

  • 使用 screenshotrepaint_boundarySelectableText 转为 PNG
  • 支持分享到社交平台

4. 动画效果

  • 输入时字符逐个“绘制”出现
  • 使用 AnimatedSwitcher 实现淡入

5. 支持 Unicode 艺术

  • 扩展至使用 , , , 等符号
  • 实现更细腻的灰度效果

✅ 总结:小工具,大创意

这个 ASCII 艺术生成器仅约 130 行代码,却完整体现了 文本处理、布局算法与用户体验设计的精妙结合

技术点 实现方式 价值
字符映射 Map<String, List<String>> 结构清晰,易于维护
横向拼接算法 按行累积拼接 保证图案对齐
等宽字体 fontFamily: 'monospace' 视觉精准还原
实时交互 onChanged + setState 流畅响应
可复制输出 Clipboard + SelectableText 提升实用性

它证明了:优秀的工具类应用,不在功能繁多,而在能否精准解决一个具体问题,并带来一丝创造的乐趣


Happy Coding with Flutter! 🐦
愿你的每一行代码,都能成为别人眼中的艺术。

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐