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


项目效果

本文实现的是一个基于 Flutter for OpenHarmony 的英语单词动态记忆卡片应用。项目中使用 Flutter 第三方库 animated_text_kit 实现动态文字效果,让英语单词、学习提示和记忆短句以打字机动画、淡入动画和彩色文字动画的形式展示。

最终运行效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

页面主要包含以下内容:

  • 顶部标题栏;
  • 动态学习标题;
  • 当前单词记忆卡片;
  • 单词释义和例句;
  • 动态学习提示语;
  • 单词掌握状态列表;
  • 学习统计信息;
  • 第三方库使用说明;
  • 页面整体采用 Flutter Material 风格布局。

本文重点是演示如何在 Flutter for OpenHarmony 项目中使用 Flutter 第三方库 animated_text_kit。项目代码写在 lib/main.dart 中,依赖配置写在 pubspec.yaml 中,符合 Flutter for OpenHarmony 第三方库实践方向。


前言

在学习类应用中,文字展示非常重要。比如单词记忆、口语练习、每日一句、课程提示、学习打卡等页面,核心内容往往都是文字。

如果所有文字都静态显示,页面虽然能用,但缺少一点吸引力。尤其是学习类应用,本来就容易让人犯困,再配上死板页面,效果就像让人对着说明书冥想,实在是很考验人类意志力。

因此本文选择使用 Flutter 第三方库 animated_text_kit 来实现动态文字效果。它可以快速实现打字机文字、淡入文字、旋转文字和彩色文字动画,适合用于标题、标语、提示语和重点内容展示。

本项目以“英语单词动态记忆卡片应用”为例,使用 animated_text_kit 展示当前学习单词、每日学习提示和动态标题,并结合 Flutter 状态更新实现单词切换和掌握状态标记。


一、项目目标

本次实践主要实现以下目标:

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加第三方库 animated_text_kit
  • 使用 flutter pub get 获取依赖;
  • lib/main.dart 中引入 animated_text_kit
  • 使用 AnimatedTextKit 构建动态文字区域;
  • 使用 TypewriterAnimatedText 实现打字机文字效果;
  • 使用 FadeAnimatedText 实现学习提示淡入效果;
  • 使用 ColorizeAnimatedText 实现彩色标题动画;
  • 实现单词切换功能;
  • 实现单词掌握状态标记;
  • 使用 Flutter Material 组件构建完整页面;
  • 将应用运行到 OpenHarmony 设备或模拟器中。

二、技术栈

类型 内容
开发方向 Flutter for OpenHarmony
开发语言 Dart
UI 框架 Flutter
第三方库 animated_text_kit
功能场景 动态文字 / 英语单词 / 学习卡片
核心组件 AnimatedTextKit / TypewriterAnimatedText / FadeAnimatedText / ColorizeAnimatedText
项目入口 lib/main.dart
依赖配置 pubspec.yaml
运行平台 OpenHarmony 设备或模拟器

三、为什么选择 animated_text_kit

在实际开发中,动态文字可以用于很多场景,例如:

  • 英语单词记忆;
  • 每日一句;
  • 学习打卡;
  • 首页欢迎语;
  • 活动标题;
  • 成就提示;
  • 应用引导页;
  • 课程宣传页;
  • 重点信息展示。

如果完全自己使用 Flutter 原生动画实现文字逐字显示,需要处理字符串拆分、定时器、动画控制器、文字刷新和动画生命周期。能写,但为了让一句话慢慢出现就手搓一堆动画逻辑,多少有点像为了喝水先研究水利工程。

animated_text_kit 已经封装好了常见文字动画组件,可以让开发者更快完成动态文字页面。

在本项目中,animated_text_kit 主要完成以下工作:

  • 展示彩色动态标题;
  • 展示打字机单词动画;
  • 展示淡入学习提示;
  • 增强学习页面的视觉表现;
  • 提升用户对重点文字的注意力。

四、创建 Flutter for OpenHarmony 项目

在已经配置好 Flutter for OpenHarmony 开发环境的前提下,可以创建一个 Flutter 项目。

示例项目名称:

flutter create vocabulary_animate_demo

进入项目目录:

cd vocabulary_animate_demo

项目创建完成后,主要关注两个文件:

vocabulary_animate_demo
 ├── pubspec.yaml
 └── lib
     └── main.dart

其中:

文件 作用
pubspec.yaml 配置 Flutter 项目依赖
lib/main.dart 编写 Flutter 页面和业务逻辑

五、添加 animated_text_kit 第三方库

打开项目根目录下的 pubspec.yaml 文件,在 dependencies 中添加 animated_text_kit

示例配置如下:

dependencies:
  flutter:
    sdk: flutter

  animated_text_kit: ^4.3.0

完整结构大致如下:

name: vocabulary_animate_demo
description: A Flutter for OpenHarmony animated text demo.
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: '>=3.4.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  animated_text_kit: ^4.3.0

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

添加完成后,在终端执行:

flutter pub get

执行成功后,就可以在 Dart 代码中使用 animated_text_kit 了。


六、项目结构

本项目主要修改 lib/main.dart 文件:

lib
 └── main.dart

本项目不需要编写 OpenHarmony 原生 ArkTS 页面,也不需要修改 Index.ets

因为这是 Flutter for OpenHarmony 项目,页面主体应该是 Flutter 代码。审核重点会看:

  • 是否使用 pubspec.yaml 添加 Flutter 第三方库;
  • 是否在 Dart 文件中 import package
  • 是否在 lib/main.dart 中实际调用第三方库;
  • 是否属于 Flutter for OpenHarmony 项目。

看到 pubspec.yamllib/main.dartimport 'package:animated_text_kit/animated_text_kit.dart';,这才是正确方向。别把 ArkTS 代码换个标题就叫 Flutter,审核员虽然痛苦,但不至于失明。


七、核心实现思路

本项目的核心流程如下:

  1. pubspec.yaml 中添加 animated_text_kit
  2. main.dart 中引入第三方库;
  3. 定义英语单词数据模型;
  4. 使用 AnimatedTextKit 展示动态标题;
  5. 使用 ColorizeAnimatedText 实现彩色标题效果;
  6. 使用 TypewriterAnimatedText 展示当前单词;
  7. 使用 FadeAnimatedText 展示学习提示语;
  8. 使用按钮切换上一个和下一个单词;
  9. 使用 setState() 更新当前单词和掌握状态。

第三方库引入代码如下:

import 'package:animated_text_kit/animated_text_kit.dart';

打字机动画核心代码如下:

AnimatedTextKit(
  key: ValueKey(currentWord.word),
  animatedTexts: [
    TypewriterAnimatedText(
      currentWord.word,
      speed: const Duration(milliseconds: 90),
    ),
  ],
  totalRepeatCount: 1,
)

彩色标题动画核心代码如下:

AnimatedTextKit(
  animatedTexts: [
    ColorizeAnimatedText(
      'Vocabulary Boost',
      colors: colorizeColors,
      textStyle: textStyle,
    ),
  ],
  repeatForever: true,
)

淡入提示动画核心代码如下:

AnimatedTextKit(
  animatedTexts: [
    FadeAnimatedText('Read it. Speak it. Use it.'),
  ],
  repeatForever: true,
)

这几段代码是本文的重点,说明项目确实使用了 Flutter 第三方库实现动态文字效果。


八、main.dart 完整代码

打开文件:

lib/main.dart

将其中内容替换为下面代码:

import 'package:animated_text_kit/animated_text_kit.dart';
import 'package:flutter/material.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Vocabulary Animate Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      home: const VocabularyHomePage(),
    );
  }
}

class VocabularyItem {
  VocabularyItem({
    required this.word,
    required this.phonetic,
    required this.meaning,
    required this.example,
    required this.tip,
    required this.icon,
    required this.color,
    this.mastered = false,
  });

  final String word;
  final String phonetic;
  final String meaning;
  final String example;
  final String tip;
  final IconData icon;
  final Color color;
  bool mastered;
}

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

  
  State<VocabularyHomePage> createState() => _VocabularyHomePageState();
}

class _VocabularyHomePageState extends State<VocabularyHomePage> {
  final List<Color> _colorizeColors = const [
    Colors.deepPurple,
    Colors.blue,
    Colors.teal,
    Colors.orange,
  ];

  final List<VocabularyItem> _words = [
    VocabularyItem(
      word: 'efficient',
      phonetic: '/ɪˈfɪʃnt/',
      meaning: 'adj. 高效的;有效率的',
      example: 'A clear plan makes study more efficient.',
      tip: '常用于描述方法、系统、工具或工作方式很高效。',
      icon: Icons.speed,
      color: Colors.deepPurple,
      mastered: true,
    ),
    VocabularyItem(
      word: 'progress',
      phonetic: '/ˈprəʊɡres/',
      meaning: 'n. 进步;进展',
      example: 'Small daily progress is still progress.',
      tip: '可以用于学习、项目、任务等各种进展场景。',
      icon: Icons.trending_up,
      color: Colors.blue,
    ),
    VocabularyItem(
      word: 'focus',
      phonetic: '/ˈfəʊkəs/',
      meaning: 'v. 集中注意力;n. 焦点',
      example: 'Focus on one task before starting another.',
      tip: '学习和工作场景中非常常用,注意搭配 focus on。',
      icon: Icons.center_focus_strong,
      color: Colors.teal,
    ),
    VocabularyItem(
      word: 'review',
      phonetic: '/rɪˈvjuː/',
      meaning: 'v. 复习;回顾;n. 评价',
      example: 'Review your notes before the exam.',
      tip: '既可以表示复习知识,也可以表示评价产品或文章。',
      icon: Icons.rate_review,
      color: Colors.orange,
    ),
    VocabularyItem(
      word: 'improve',
      phonetic: '/ɪmˈpruːv/',
      meaning: 'v. 提高;改善',
      example: 'Speaking every day can improve your English.',
      tip: '后面可以直接接能力、成绩、方法或状态。',
      icon: Icons.upgrade,
      color: Colors.green,
    ),
  ];

  int _currentIndex = 0;

  VocabularyItem get _currentWord {
    return _words[_currentIndex];
  }

  int get _masteredCount {
    return _words.where((word) => word.mastered).length;
  }

  int get _unmasteredCount {
    return _words.length - _masteredCount;
  }

  double get _masteredProgress {
    if (_words.isEmpty) {
      return 0;
    }

    return _masteredCount / _words.length;
  }

  void _goPrevious() {
    setState(() {
      if (_currentIndex == 0) {
        _currentIndex = _words.length - 1;
      } else {
        _currentIndex--;
      }
    });
  }

  void _goNext() {
    setState(() {
      if (_currentIndex == _words.length - 1) {
        _currentIndex = 0;
      } else {
        _currentIndex++;
      }
    });
  }

  void _toggleMastered() {
    setState(() {
      _currentWord.mastered = !_currentWord.mastered;
    });
  }

  void _selectWord(int index) {
    setState(() {
      _currentIndex = index;
    });
  }

  void _resetWords() {
    setState(() {
      for (final VocabularyItem word in _words) {
        word.mastered = false;
      }
      _currentIndex = 0;
    });
  }

  
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final VocabularyItem currentWord = _currentWord;

    return Scaffold(
      appBar: AppBar(
        title: const Text('英语单词动态记忆'),
        centerTitle: true,
      ),
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            _buildHeaderCard(theme),
            const SizedBox(height: 16),
            _buildWordCard(theme, currentWord),
            const SizedBox(height: 16),
            _buildActionCard(theme),
            const SizedBox(height: 16),
            _buildWordListCard(theme),
            const SizedBox(height: 16),
            _buildLibraryCard(theme),
          ],
        ),
      ),
    );
  }

  Widget _buildHeaderCard(ThemeData theme) {
    final TextStyle colorizeTextStyle =
        theme.textTheme.headlineSmall!.copyWith(
      fontWeight: FontWeight.bold,
    );

    return Card(
      elevation: 3,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(22),
      ),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            Container(
              width: 76,
              height: 76,
              decoration: BoxDecoration(
                color: theme.colorScheme.primaryContainer,
                borderRadius: BorderRadius.circular(24),
              ),
              child: Icon(
                Icons.auto_stories,
                size: 42,
                color: theme.colorScheme.onPrimaryContainer,
              ),
            ),
            const SizedBox(height: 18),
            AnimatedTextKit(
              animatedTexts: [
                ColorizeAnimatedText(
                  'Vocabulary Boost',
                  textStyle: colorizeTextStyle,
                  colors: _colorizeColors,
                  speed: const Duration(milliseconds: 450),
                ),
              ],
              repeatForever: true,
              pause: const Duration(milliseconds: 600),
            ),
            const SizedBox(height: 10),
            AnimatedTextKit(
              animatedTexts: [
                FadeAnimatedText(
                  'Read it. Speak it. Use it.',
                  textStyle: theme.textTheme.bodyLarge?.copyWith(
                    color: theme.colorScheme.onSurfaceVariant,
                    fontWeight: FontWeight.w600,
                  ),
                  duration: const Duration(milliseconds: 1800),
                ),
                FadeAnimatedText(
                  '每天记一点,别指望大脑自动开窍。',
                  textStyle: theme.textTheme.bodyLarge?.copyWith(
                    color: theme.colorScheme.onSurfaceVariant,
                    fontWeight: FontWeight.w600,
                  ),
                  duration: const Duration(milliseconds: 1800),
                ),
              ],
              repeatForever: true,
              pause: const Duration(milliseconds: 600),
            ),
            const SizedBox(height: 22),
            Row(
              children: [
                _buildStatItem(
                  theme,
                  title: '单词总数',
                  value: '${_words.length}',
                  icon: Icons.menu_book,
                ),
                _buildStatItem(
                  theme,
                  title: '已掌握',
                  value: '$_masteredCount',
                  icon: Icons.check_circle,
                ),
                _buildStatItem(
                  theme,
                  title: '待复习',
                  value: '$_unmasteredCount',
                  icon: Icons.pending_actions,
                ),
              ],
            ),
            const SizedBox(height: 18),
            LinearProgressIndicator(
              value: _masteredProgress,
              minHeight: 10,
              borderRadius: BorderRadius.circular(10),
            ),
            const SizedBox(height: 8),
            Align(
              alignment: Alignment.centerRight,
              child: Text(
                '掌握进度 ${(100 * _masteredProgress).toStringAsFixed(0)}%',
                style: theme.textTheme.bodySmall?.copyWith(
                  color: theme.colorScheme.primary,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildStatItem(
    ThemeData theme, {
    required String title,
    required String value,
    required IconData icon,
  }) {
    return Expanded(
      child: Column(
        children: [
          Icon(
            icon,
            color: theme.colorScheme.primary,
          ),
          const SizedBox(height: 6),
          Text(
            value,
            style: theme.textTheme.titleLarge?.copyWith(
              fontWeight: FontWeight.bold,
              color: theme.colorScheme.primary,
            ),
          ),
          const SizedBox(height: 2),
          Text(
            title,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildWordCard(ThemeData theme, VocabularyItem word) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(22),
        child: Column(
          children: [
            Container(
              width: 72,
              height: 72,
              decoration: BoxDecoration(
                color: word.color.withOpacity(0.16),
                borderRadius: BorderRadius.circular(24),
              ),
              child: Icon(
                word.icon,
                size: 38,
                color: word.color,
              ),
            ),
            const SizedBox(height: 20),
            SizedBox(
              height: 48,
              child: Center(
                child: AnimatedTextKit(
                  key: ValueKey(word.word),
                  animatedTexts: [
                    TypewriterAnimatedText(
                      word.word,
                      textStyle: theme.textTheme.headlineMedium?.copyWith(
                        fontWeight: FontWeight.bold,
                        color: word.color,
                      ),
                      speed: const Duration(milliseconds: 90),
                    ),
                  ],
                  totalRepeatCount: 1,
                  displayFullTextOnTap: true,
                  stopPauseOnTap: true,
                ),
              ),
            ),
            const SizedBox(height: 6),
            Text(
              word.phonetic,
              style: theme.textTheme.titleMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                fontWeight: FontWeight.w600,
              ),
            ),
            const SizedBox(height: 18),
            _buildInfoBlock(
              theme,
              title: '中文释义',
              content: word.meaning,
              icon: Icons.translate,
              color: word.color,
            ),
            const SizedBox(height: 12),
            _buildInfoBlock(
              theme,
              title: '例句',
              content: word.example,
              icon: Icons.format_quote,
              color: word.color,
            ),
            const SizedBox(height: 12),
            _buildInfoBlock(
              theme,
              title: '记忆提示',
              content: word.tip,
              icon: Icons.lightbulb,
              color: word.color,
            ),
            const SizedBox(height: 18),
            Container(
              padding: const EdgeInsets.symmetric(
                horizontal: 14,
                vertical: 8,
              ),
              decoration: BoxDecoration(
                color: word.mastered
                    ? Colors.green.withOpacity(0.14)
                    : Colors.orange.withOpacity(0.14),
                borderRadius: BorderRadius.circular(20),
              ),
              child: Text(
                word.mastered ? '当前状态:已掌握' : '当前状态:待复习',
                style: theme.textTheme.bodyMedium?.copyWith(
                  color: word.mastered ? Colors.green : Colors.orange,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoBlock(
    ThemeData theme, {
    required String title,
    required String content,
    required IconData icon,
    required Color color,
  }) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: color.withOpacity(0.10),
        borderRadius: BorderRadius.circular(16),
        border: Border.all(
          color: color.withOpacity(0.22),
        ),
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Icon(
            icon,
            color: color,
            size: 24,
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: color,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 6),
                Text(
                  content,
                  style: theme.textTheme.bodyMedium?.copyWith(
                    color: theme.colorScheme.onSurfaceVariant,
                    height: 1.5,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildActionCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            Row(
              children: [
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: _goPrevious,
                    icon: const Icon(Icons.arrow_back),
                    label: const Text('上一个'),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: _goNext,
                    icon: const Icon(Icons.arrow_forward),
                    label: const Text('下一个'),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: _toggleMastered,
                    icon: Icon(
                      _currentWord.mastered
                          ? Icons.undo
                          : Icons.check_circle,
                    ),
                    label: Text(
                      _currentWord.mastered ? '取消掌握' : '标记掌握',
                    ),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: _resetWords,
                    icon: const Icon(Icons.refresh),
                    label: const Text('重置状态'),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildWordListCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '单词列表',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 12),
            ...List.generate(_words.length, (index) {
              final VocabularyItem word = _words[index];
              final bool selected = index == _currentIndex;

              return Container(
                margin: const EdgeInsets.only(bottom: 10),
                child: InkWell(
                  borderRadius: BorderRadius.circular(16),
                  onTap: () {
                    _selectWord(index);
                  },
                  child: Container(
                    padding: const EdgeInsets.all(14),
                    decoration: BoxDecoration(
                      color: selected
                          ? word.color.withOpacity(0.14)
                          : theme.colorScheme.surfaceContainerHighest,
                      borderRadius: BorderRadius.circular(16),
                      border: Border.all(
                        color: selected ? word.color : Colors.transparent,
                        width: 1.2,
                      ),
                    ),
                    child: Row(
                      children: [
                        Icon(
                          word.icon,
                          color: word.color,
                        ),
                        const SizedBox(width: 12),
                        Expanded(
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text(
                                word.word,
                                style: theme.textTheme.titleMedium?.copyWith(
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                              const SizedBox(height: 4),
                              Text(
                                word.meaning,
                                style: theme.textTheme.bodySmall?.copyWith(
                                  color: theme.colorScheme.onSurfaceVariant,
                                ),
                              ),
                            ],
                          ),
                        ),
                        Icon(
                          word.mastered
                              ? Icons.check_circle
                              : Icons.radio_button_unchecked,
                          color: word.mastered
                              ? Colors.green
                              : theme.colorScheme.onSurfaceVariant,
                        ),
                      ],
                    ),
                  ),
                ),
              );
            }),
          ],
        ),
      ),
    );
  }

  Widget _buildLibraryCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '第三方库说明',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 12),
            _buildInfoRow(
              theme,
              title: '库名称',
              value: 'animated_text_kit',
            ),
            _buildInfoRow(
              theme,
              title: '配置文件',
              value: 'pubspec.yaml',
            ),
            _buildInfoRow(
              theme,
              title: '导入方式',
              value: "import 'package:animated_text_kit/animated_text_kit.dart';",
            ),
            _buildInfoRow(
              theme,
              title: '核心组件',
              value:
                  'AnimatedTextKit / TypewriterAnimatedText / FadeAnimatedText / ColorizeAnimatedText',
            ),
            _buildInfoRow(
              theme,
              title: '应用场景',
              value: '动态标题、单词记忆、每日一句、学习提示、活动标语',
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoRow(
    ThemeData theme, {
    required String title,
    required String value,
  }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 10),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 82,
            child: Text(
              title,
              style: theme.textTheme.bodyMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          Expanded(
            child: Text(
              value,
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

九、代码实现说明

1. 引入 animated_text_kit 第三方库

代码开头引入第三方库:

import 'package:animated_text_kit/animated_text_kit.dart';

这说明项目确实使用了 Flutter 第三方库,而不是 OpenHarmony 原生库。

本项目中主要使用以下组件:

AnimatedTextKit
TypewriterAnimatedText
FadeAnimatedText
ColorizeAnimatedText

其中:

组件 作用
AnimatedTextKit 文字动画容器
TypewriterAnimatedText 打字机文字动画
FadeAnimatedText 淡入淡出文字动画
ColorizeAnimatedText 彩色渐变文字动画

2. 定义英语单词数据模型

项目中定义了单词模型:

class VocabularyItem {
  VocabularyItem({
    required this.word,
    required this.phonetic,
    required this.meaning,
    required this.example,
    required this.tip,
    required this.icon,
    required this.color,
    this.mastered = false,
  });

  final String word;
  final String phonetic;
  final String meaning;
  final String example;
  final String tip;
  final IconData icon;
  final Color color;
  bool mastered;
}

字段说明如下:

字段 作用
word 英语单词
phonetic 音标
meaning 中文释义
example 英文例句
tip 记忆提示
icon 单词图标
color 单词主题色
mastered 是否已经掌握

其中 mastered 是可变化状态,用来记录当前单词是否已经掌握。


3. 使用 AnimatedTextKit 作为动画容器

所有文字动画都放在 AnimatedTextKit 中,例如:

AnimatedTextKit(
  animatedTexts: [
    TypewriterAnimatedText('efficient'),
  ],
)

AnimatedTextKit 本身负责播放动画,里面的 animatedTexts 决定播放哪一种文字动画。


4. 使用 TypewriterAnimatedText 实现打字机效果

当前单词使用打字机动画展示:

AnimatedTextKit(
  key: ValueKey(word.word),
  animatedTexts: [
    TypewriterAnimatedText(
      word.word,
      speed: const Duration(milliseconds: 90),
    ),
  ],
  totalRepeatCount: 1,
)

其中:

参数 作用
key 单词变化时重新播放动画
animatedTexts 动画文字列表
speed 每个字符出现速度
totalRepeatCount 动画播放次数

这里使用:

key: ValueKey(word.word)

是为了让切换单词时,打字机动画可以重新播放。

如果不加 key,有时页面更新了但动画不重新开始。Flutter 不是魔法,它只按规则重建,别指望它理解人类“我想再播一次”的复杂心情。


5. 使用 FadeAnimatedText 展示学习提示

页面顶部使用淡入动画展示学习提示:

AnimatedTextKit(
  animatedTexts: [
    FadeAnimatedText('Read it. Speak it. Use it.'),
    FadeAnimatedText('每天记一点,别指望大脑自动开窍。'),
  ],
  repeatForever: true,
)

这里设置:

repeatForever: true

表示提示文字会循环播放。

适合用于学习提醒、标语、欢迎语等场景。


6. 使用 ColorizeAnimatedText 展示彩色标题

页面标题使用彩色文字动画:

ColorizeAnimatedText(
  'Vocabulary Boost',
  textStyle: colorizeTextStyle,
  colors: _colorizeColors,
)

颜色列表如下:

final List<Color> _colorizeColors = const [
  Colors.deepPurple,
  Colors.blue,
  Colors.teal,
  Colors.orange,
];

ColorizeAnimatedText 会让文字颜色动态变化,使标题更有视觉重点。


7. 实现单词切换功能

上一个单词:

void _goPrevious() {
  setState(() {
    if (_currentIndex == 0) {
      _currentIndex = _words.length - 1;
    } else {
      _currentIndex--;
    }
  });
}

下一个单词:

void _goNext() {
  setState(() {
    if (_currentIndex == _words.length - 1) {
      _currentIndex = 0;
    } else {
      _currentIndex++;
    }
  });
}

这里做了循环切换。到第一个单词时继续点“上一个”,会跳到最后一个单词;到最后一个单词时继续点“下一个”,会回到第一个单词。


8. 实现掌握状态标记

点击“标记掌握”按钮后执行:

void _toggleMastered() {
  setState(() {
    _currentWord.mastered = !_currentWord.mastered;
  });
}

如果当前单词未掌握,会标记为已掌握。

如果当前单词已经掌握,会取消掌握状态。

页面中的统计数据、进度条和单词列表状态都会跟着更新。


9. 计算学习进度

已掌握数量:

int get _masteredCount {
  return _words.where((word) => word.mastered).length;
}

掌握进度:

double get _masteredProgress {
  if (_words.isEmpty) {
    return 0;
  }

  return _masteredCount / _words.length;
}

进度条展示:

LinearProgressIndicator(
  value: _masteredProgress,
)

这样可以直观看到当前单词掌握情况。


10. 单词列表点击切换

单词列表中点击某一项后执行:

void _selectWord(int index) {
  setState(() {
    _currentIndex = index;
  });
}

这样用户不仅可以通过“上一个”“下一个”切换单词,也可以直接点击列表中的单词进行查看。


十、运行项目

完成代码后,在终端执行:

flutter pub get

然后连接 OpenHarmony 设备或启动 OpenHarmony 模拟器。

查看设备:

flutter devices

运行项目:

flutter run

如果环境配置正确,应用会运行到 OpenHarmony 设备或模拟器中。

运行成功后,页面会显示“英语单词动态记忆”。用户可以点击“上一个”“下一个”切换单词,也可以点击“标记掌握”更新学习状态。页面中的标题、提示语和当前单词会通过 animated_text_kit 展示动态文字效果。


十一、开发中遇到的问题

1. animated_text_kit 依赖没有生效

如果代码中出现找不到 animated_text_kit 的问题,可以检查 pubspec.yaml 中是否添加了:

animated_text_kit: ^4.3.0

然后重新执行:

flutter pub get

如果还是不行,可以重启编辑器。编辑器有时像刚从午睡里醒来,依赖明明装了,它还一脸茫然,真让人安心。


2. import 导入报错

如果下面代码报错:

import 'package:animated_text_kit/animated_text_kit.dart';

通常有几种原因:

  • pubspec.yaml 中没有添加依赖;
  • 没有执行 flutter pub get
  • YAML 缩进错误;
  • 包名写错;
  • 编辑器没有刷新依赖。

其中 YAML 缩进最容易出问题。依赖必须写在 dependencies 下面,并且缩进要正确。一个空格就能毁掉项目运行,编程世界真是体贴得令人窒息。


3. 文字动画没有播放

如果文字动画没有播放,可以检查:

  • 是否使用了 AnimatedTextKit
  • animatedTexts 中是否添加了动画文字;
  • 是否设置了有效的 textStyle
  • 动画组件是否被其他组件遮挡;
  • 页面是否成功运行。

基础结构应该类似:

AnimatedTextKit(
  animatedTexts: [
    TypewriterAnimatedText('Hello'),
  ],
)

4. 切换单词后动画没有重新播放

如果切换单词后,打字机动画没有重新播放,可以给 AnimatedTextKit 添加 key:

key: ValueKey(word.word)

这样当前单词变化时,Flutter 会把它识别为新的动画组件,从而重新播放动画。


5. 彩色文字不显示

如果 ColorizeAnimatedText 没有正常显示,可以检查是否设置了:

textStyle
colors

例如:

ColorizeAnimatedText(
  'Vocabulary Boost',
  textStyle: colorizeTextStyle,
  colors: _colorizeColors,
)

颜色列表至少要有多个颜色,否则彩色变化效果不明显。只给一个颜色还期待它渐变,那属于对颜色提出了不合理劳动要求。


6. 中文和英文混排显示不整齐

如果中文和英文混排显示效果不好,可以调整:

  • 字体大小;
  • 行高;
  • 文本对齐方式;
  • 卡片宽度;
  • 外边距和内边距。

本项目中主要通过 height: 1.5 改善多行文字可读性。


7. 点击按钮后状态没有变化

如果点击“标记掌握”后页面没有变化,可以检查是否调用了:

setState(() {
  _currentWord.mastered = !_currentWord.mastered;
});

Flutter 中状态变化后必须调用 setState(),否则页面不会刷新。它只是框架,不是读心术机器。


8. 运行不到 OpenHarmony 设备

如果项目无法运行到 OpenHarmony 设备或模拟器,可以检查:

  • Flutter for OpenHarmony 环境是否配置完成;
  • 设备是否连接成功;
  • flutter devices 是否能识别设备;
  • 是否执行了 flutter pub get
  • 是否选择了正确的运行设备;
  • 项目是否为 Flutter 项目,而不是原生鸿蒙项目。

如果 flutter devices 都识别不到设备,那应该先处理环境问题,而不是盯着文字动画代码反复怀疑人生。动画很无辜,至少这次大概率是。


十二、本文和原生鸿蒙项目的区别

本文是 Flutter for OpenHarmony 第三方库实践,不是 OpenHarmony 原生 ArkTS 项目。

主要区别如下:

对比项 本文写法 原生鸿蒙写法
UI 技术 Flutter ArkUI
主要语言 Dart ArkTS
页面入口 lib/main.dart Index.ets
依赖配置 pubspec.yaml oh-package.json5
依赖安装 flutter pub get ohpm install
第三方库 animated_text_kit OpenHarmony 原生库
页面组件 MaterialApp / Scaffold / AnimatedTextKit @Entry / @Component

因此本文符合 Flutter for OpenHarmony 第三方库实践方向。


十三、总结

本篇完成了一个基于 animated_text_kit 的 Flutter for OpenHarmony 英语单词动态记忆卡片应用。项目通过 Flutter 第三方库实现动态文字效果,并结合单词切换、掌握状态标记和学习进度展示构建了一个完整的学习类页面。

通过本次实践,我主要完成了以下内容:

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加 animated_text_kit 依赖;
  • 使用 flutter pub get 获取第三方库;
  • lib/main.dart 中引入 animated_text_kit
  • 使用 AnimatedTextKit 构建动态文字区域;
  • 使用 TypewriterAnimatedText 实现打字机单词动画;
  • 使用 FadeAnimatedText 实现学习提示动画;
  • 使用 ColorizeAnimatedText 实现彩色标题动画;
  • 使用 setState() 实现单词切换和状态更新;
  • 使用 Flutter Material 组件构建完整页面;
  • 将项目运行到 OpenHarmony 设备或模拟器中。

这个项目虽然只是一个基础单词记忆应用,但完整展示了 Flutter for OpenHarmony 项目中第三方库的使用流程。

后续可以在这个基础上继续扩展,例如:

  • 添加更多单词数据;
  • 添加单词搜索功能;
  • 添加错题本;
  • 添加本地数据保存;
  • 添加每日学习目标;
  • 添加发音音频;
  • 添加随机测试;
  • 添加学习打卡;
  • 添加暗色主题;
  • 添加云端同步。

整体来看,animated_text_kit 可以帮助 Flutter 开发者快速实现动态文字展示。通过这个项目,可以理解 Flutter for OpenHarmony 中第三方库依赖配置、动态文字组件使用和页面状态更新之间的基本关系。

Logo

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

更多推荐