欢迎加入开源鸿蒙跨平台社区:
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 原生动画实现打字机效果、淡入淡出效果、彩色文字动画,就需要处理动画控制器、字符显示进度、动画循环、文本切换等逻辑。能写,但为了几个字去手搓动画控制器,多少有点像为了吃个苹果先种一棵树。

因此本文选择使用 Flutter 第三方库 animated_text_kit 来实现文字动画。它提供了多种常见文字动画组件,可以快速实现打字机、淡入淡出、彩色渐变、波浪文字等效果,非常适合 Flutter for OpenHarmony 项目中的首页标题、提示语和任务说明场景。

本项目以“学习计划动态标语应用”为例,使用 animated_text_kit 展示动态学习标语,并结合任务统计、任务列表、任务筛选和当前任务详情完成一个完整页面。


一、项目目标

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

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加第三方库 animated_text_kit
  • 使用 flutter pub get 获取依赖;
  • lib/main.dart 中引入 animated_text_kit
  • 使用 AnimatedTextKit 构建动态文字区域;
  • 使用 TypewriterAnimatedText 实现打字机文字效果;
  • 使用 FadeAnimatedText 实现淡入淡出文字效果;
  • 使用 ColorizeAnimatedText 实现彩色文字动画;
  • 使用 WavyAnimatedText 实现波浪文字效果;
  • 使用 TyperAnimatedText 实现逐字显示提示;
  • 实现学习任务勾选和统计;
  • 实现未完成任务筛选;
  • 使用 Flutter Material 组件构建完整页面;
  • 将应用运行到 OpenHarmony 设备或模拟器中。

二、技术栈

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

三、为什么选择 animated_text_kit

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

  • 应用启动页;
  • 首页欢迎语;
  • 学习打卡页面;
  • 活动宣传页;
  • 搜索空状态提示;
  • 任务提醒页面;
  • 课程推荐标题;
  • 数据加载提示;
  • 用户引导页面;
  • 个人主页标语。

Flutter 原生可以通过 AnimationController 实现文字动画,但代码量相对较大,而且不同类型动画需要分别处理。

animated_text_kit 已经封装好了常见文字动画效果,可以直接通过 AnimatedTextKit 组合不同动画文字。

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

  • 展示首页动态标语;
  • 实现打字机文字效果;
  • 实现淡入淡出提示语;
  • 实现彩色渐变标题;
  • 实现波浪文字提示;
  • 实现任务详情中的动态状态提示;
  • 让学习计划页面更有动态效果;
  • 减少手写动画代码量。

四、创建 Flutter for OpenHarmony 项目

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

示例项目名称:

flutter create study_text_demo

进入项目目录:

cd study_text_demo

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

study_text_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: study_text_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';,这才是正确方向。不是写一堆普通 Text 组件就能骗过审核,审核虽然累,但还没完全失明。


七、核心实现思路

本项目的核心流程如下:

  1. pubspec.yaml 中添加 animated_text_kit
  2. main.dart 中引入第三方库;
  3. 定义学习任务数据模型;
  4. 准备多条学习任务数据;
  5. 使用 AnimatedTextKit 构建动态标题;
  6. 使用 TypewriterAnimatedText 实现打字机效果;
  7. 使用 FadeAnimatedText 实现提示语切换;
  8. 使用 ColorizeAnimatedText 实现彩色文字动画;
  9. 使用 WavyAnimatedText 实现空状态提示;
  10. 使用 TyperAnimatedText 实现任务详情提示;
  11. 使用 setState() 更新任务完成状态;
  12. 使用筛选开关展示未完成任务;
  13. 使用 Flutter Material 组件构建完整页面。

第三方库引入代码如下:

import 'package:animated_text_kit/animated_text_kit.dart';

动态文字核心代码如下:

AnimatedTextKit(
  repeatForever: true,
  pause: const Duration(milliseconds: 900),
  displayFullTextOnTap: true,
  stopPauseOnTap: true,
  animatedTexts: [
    TypewriterAnimatedText(
      '今天先完成一个小目标',
      textStyle: titleStyle,
      speed: const Duration(milliseconds: 80),
    ),
    FadeAnimatedText(
      '学习不靠玄学,靠重复',
      textStyle: titleStyle,
    ),
    ColorizeAnimatedText(
      'Flutter for OpenHarmony',
      textStyle: colorizeTextStyle,
      colors: colorizeColors,
    ),
  ],
)

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


八、main.dart 完整代码

打开文件:

lib/main.dart

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

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

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

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

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

class StudyTask {
  StudyTask({
    required this.id,
    required this.title,
    required this.subject,
    required this.description,
    required this.minutes,
    required this.icon,
    required this.color,
    this.done = false,
  });

  final int id;
  final String title;
  final String subject;
  final String description;
  final int minutes;
  final IconData icon;
  final Color color;
  bool done;
}

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

  
  State<StudyHomePage> createState() => _StudyHomePageState();
}

class _StudyHomePageState extends State<StudyHomePage> {
  static const List<Color> _colorizeColors = [
    Colors.indigo,
    Colors.purple,
    Colors.blue,
    Colors.teal,
  ];

  static const TextStyle _colorizeTextStyle = TextStyle(
    fontSize: 22,
    fontWeight: FontWeight.bold,
  );

  bool _showOnlyTodo = false;
  int _selectedTaskId = 1;
  int _textSeed = 0;

  final List<StudyTask> _tasks = [
    StudyTask(
      id: 1,
      title: '复习 Dart 基础语法',
      subject: 'Flutter',
      description: '整理变量、函数、类、List、Map 的基本用法,重点看自己容易忘的地方。',
      minutes: 35,
      icon: Icons.code,
      color: Colors.indigo,
      done: true,
    ),
    StudyTask(
      id: 2,
      title: '完成一个页面小练习',
      subject: 'OpenHarmony',
      description: '把今天的 Flutter 第三方库示例运行起来,并截图放到文章开头。',
      minutes: 45,
      icon: Icons.phone_iphone,
      color: Colors.blue,
    ),
    StudyTask(
      id: 3,
      title: '整理英文表达',
      subject: 'English',
      description: '记录 5 个今天能用到的英文句子,不要只背单词,句子才更容易拿来开口。',
      minutes: 20,
      icon: Icons.translate,
      color: Colors.green,
    ),
    StudyTask(
      id: 4,
      title: '阅读技术文章',
      subject: 'Reading',
      description: '阅读一篇 Flutter 相关技术文章,记录第三方库名称、作用和核心组件。',
      minutes: 25,
      icon: Icons.menu_book,
      color: Colors.orange,
    ),
    StudyTask(
      id: 5,
      title: '检查项目运行效果',
      subject: 'Debug',
      description: '运行项目后检查页面是否能正常显示,确认截图中能看出第三方库效果。',
      minutes: 30,
      icon: Icons.bug_report,
      color: Colors.redAccent,
    ),
  ];

  List<StudyTask> get _visibleTasks {
    if (_showOnlyTodo) {
      return _tasks.where((task) => !task.done).toList();
    }

    return _tasks;
  }

  StudyTask get _selectedTask {
    return _tasks.firstWhere(
      (task) {
        return task.id == _selectedTaskId;
      },
      orElse: () {
        return _tasks.first;
      },
    );
  }

  int get _doneCount {
    return _tasks.where((task) => task.done).length;
  }

  int get _todoCount {
    return _tasks.length - _doneCount;
  }

  int get _totalMinutes {
    int total = 0;

    for (final StudyTask task in _tasks) {
      total += task.minutes;
    }

    return total;
  }

  double get _progressValue {
    if (_tasks.isEmpty) {
      return 0;
    }

    return _doneCount / _tasks.length;
  }

  void _toggleTask(StudyTask task) {
    setState(() {
      task.done = !task.done;
      _selectedTaskId = task.id;
    });
  }

  void _selectTask(StudyTask task) {
    setState(() {
      _selectedTaskId = task.id;
    });
  }

  void _toggleFilter(bool value) {
    setState(() {
      _showOnlyTodo = value;

      final List<StudyTask> visibleTasks = _visibleTasks;
      if (visibleTasks.isNotEmpty) {
        _selectedTaskId = visibleTasks.first.id;
      }
    });
  }

  void _restartTextAnimation() {
    setState(() {
      _textSeed++;
    });

    ScaffoldMessenger.of(context)
      ..clearSnackBars()
      ..showSnackBar(
        const SnackBar(
          content: Text('动态文字已重新播放'),
          behavior: SnackBarBehavior.floating,
          duration: Duration(milliseconds: 1200),
        ),
      );
  }

  void _finishSelectedTask() {
    setState(() {
      _selectedTask.done = true;
    });
  }

  String get _progressText {
    if (_doneCount == _tasks.length) {
      return '今日任务已全部完成';
    }

    if (_doneCount == 0) {
      return '还没开始,先动第一步';
    }

    return '已完成 $_doneCount 项,还剩 $_todoCount 项';
  }

  
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final List<StudyTask> visibleTasks = _visibleTasks;

    return Scaffold(
      appBar: AppBar(
        title: const Text('学习计划动态标语'),
        centerTitle: true,
      ),
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            _buildAnimatedHeader(theme),
            const SizedBox(height: 16),
            _buildOverviewCard(theme),
            const SizedBox(height: 16),
            _buildFilterCard(theme),
            const SizedBox(height: 16),
            _buildTaskListCard(theme, visibleTasks),
            const SizedBox(height: 16),
            _buildSelectedTaskCard(theme),
            const SizedBox(height: 16),
            _buildActionCard(theme),
            const SizedBox(height: 16),
            _buildLibraryCard(theme),
          ],
        ),
      ),
    );
  }

  Widget _buildAnimatedHeader(ThemeData theme) {
    final TextStyle titleStyle = theme.textTheme.titleLarge!.copyWith(
      fontWeight: FontWeight.bold,
      color: theme.colorScheme.primary,
    );

    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_awesome,
                size: 42,
                color: theme.colorScheme.onPrimaryContainer,
              ),
            ),
            const SizedBox(height: 18),
            SizedBox(
              height: 64,
              child: Center(
                child: AnimatedTextKit(
                  key: ValueKey(_textSeed),
                  repeatForever: true,
                  pause: const Duration(milliseconds: 900),
                  displayFullTextOnTap: true,
                  stopPauseOnTap: true,
                  animatedTexts: [
                    TypewriterAnimatedText(
                      '今天先完成一个小目标',
                      textStyle: titleStyle,
                      speed: const Duration(milliseconds: 80),
                      textAlign: TextAlign.center,
                    ),
                    FadeAnimatedText(
                      '学习不靠玄学,靠重复',
                      textStyle: titleStyle,
                      textAlign: TextAlign.center,
                    ),
                    ColorizeAnimatedText(
                      'Flutter for OpenHarmony',
                      textStyle: _colorizeTextStyle,
                      colors: _colorizeColors,
                      textAlign: TextAlign.center,
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 10),
            Text(
              '使用 animated_text_kit 构建动态文字学习计划页面',
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                height: 1.5,
              ),
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildOverviewCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            Row(
              children: [
                Icon(
                  Icons.track_changes,
                  color: theme.colorScheme.primary,
                ),
                const SizedBox(width: 10),
                Expanded(
                  child: Text(
                    '今日学习进度',
                    style: theme.textTheme.titleLarge?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            LinearProgressIndicator(
              value: _progressValue,
              minHeight: 9,
              borderRadius: BorderRadius.circular(9),
            ),
            const SizedBox(height: 12),
            Text(
              _progressText,
              style: theme.textTheme.titleMedium?.copyWith(
                color: theme.colorScheme.primary,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 18),
            Row(
              children: [
                _buildStatItem(
                  theme,
                  title: '总任务',
                  value: '${_tasks.length}',
                  icon: Icons.list_alt,
                ),
                _buildStatItem(
                  theme,
                  title: '已完成',
                  value: '$_doneCount',
                  icon: Icons.done_all,
                ),
                _buildStatItem(
                  theme,
                  title: '未完成',
                  value: '$_todoCount',
                  icon: Icons.pending_actions,
                ),
                _buildStatItem(
                  theme,
                  title: '预计',
                  value: '$_totalMinutes分',
                  icon: Icons.timer,
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  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.titleMedium?.copyWith(
              fontWeight: FontWeight.bold,
              color: theme.colorScheme.primary,
            ),
            overflow: TextOverflow.ellipsis,
          ),
          const SizedBox(height: 2),
          Text(
            title,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildFilterCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: SwitchListTile(
        value: _showOnlyTodo,
        onChanged: _toggleFilter,
        secondary: Icon(
          Icons.filter_alt,
          color: theme.colorScheme.primary,
        ),
        title: const Text('只显示未完成任务'),
        subtitle: const Text('用于快速查看今天还需要继续处理的内容'),
      ),
    );
  }

  Widget _buildTaskListCard(
    ThemeData theme,
    List<StudyTask> visibleTasks,
  ) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.fromLTRB(16, 20, 16, 8),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Expanded(
                  child: Text(
                    '今日学习任务',
                    style: theme.textTheme.titleLarge?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                Text(
                  '显示 ${visibleTasks.length} 项',
                  style: theme.textTheme.bodyMedium?.copyWith(
                    color: theme.colorScheme.primary,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 14),
            if (visibleTasks.isEmpty)
              Padding(
                padding: const EdgeInsets.all(18),
                child: Center(
                  child: AnimatedTextKit(
                    repeatForever: true,
                    animatedTexts: [
                      WavyAnimatedText(
                        '没有未完成任务',
                        textStyle: theme.textTheme.titleMedium!.copyWith(
                          color: theme.colorScheme.primary,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ],
                  ),
                ),
              )
            else
              ...visibleTasks.map((task) {
                return _buildTaskItem(theme, task);
              }),
          ],
        ),
      ),
    );
  }

  Widget _buildTaskItem(ThemeData theme, StudyTask task) {
    final bool selected = task.id == _selectedTaskId;

    return GestureDetector(
      onTap: () {
        _selectTask(task);
      },
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 220),
        margin: const EdgeInsets.only(bottom: 12),
        padding: const EdgeInsets.all(14),
        decoration: BoxDecoration(
          color: selected
              ? task.color.withOpacity(0.16)
              : task.color.withOpacity(0.08),
          borderRadius: BorderRadius.circular(18),
          border: Border.all(
            color: selected
                ? task.color.withOpacity(0.70)
                : task.color.withOpacity(0.20),
            width: selected ? 2 : 1,
          ),
        ),
        child: Row(
          children: [
            Container(
              width: 54,
              height: 54,
              decoration: BoxDecoration(
                color: task.color.withOpacity(0.18),
                borderRadius: BorderRadius.circular(18),
              ),
              child: Icon(
                task.icon,
                color: task.color,
                size: 29,
              ),
            ),
            const SizedBox(width: 14),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    task.title,
                    style: theme.textTheme.titleMedium?.copyWith(
                      fontWeight: FontWeight.bold,
                      decoration:
                          task.done ? TextDecoration.lineThrough : null,
                    ),
                  ),
                  const SizedBox(height: 5),
                  Text(
                    '${task.subject} · ${task.minutes} 分钟',
                    style: theme.textTheme.bodySmall?.copyWith(
                      color: theme.colorScheme.onSurfaceVariant,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    task.description,
                    style: theme.textTheme.bodyMedium?.copyWith(
                      color: theme.colorScheme.onSurfaceVariant,
                      height: 1.45,
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(width: 8),
            Checkbox(
              value: task.done,
              onChanged: (_) {
                _toggleTask(task);
              },
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSelectedTaskCard(ThemeData theme) {
    final StudyTask task = _selectedTask;

    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            Row(
              children: [
                Icon(
                  task.icon,
                  color: task.color,
                ),
                const SizedBox(width: 10),
                Expanded(
                  child: Text(
                    '当前选中任务',
                    style: theme.textTheme.titleLarge?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 18),
            SizedBox(
              height: 38,
              child: Center(
                child: AnimatedTextKit(
                  key: ValueKey('${task.id}-${task.done}'),
                  isRepeatingAnimation: false,
                  animatedTexts: [
                    TyperAnimatedText(
                      task.done ? '这个任务已经完成' : '这个任务还需要继续推进',
                      textStyle: theme.textTheme.titleMedium!.copyWith(
                        color: task.color,
                        fontWeight: FontWeight.bold,
                      ),
                      speed: const Duration(milliseconds: 70),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 12),
            _buildDetailRow(
              theme,
              title: '任务',
              value: task.title,
              color: task.color,
            ),
            _buildDetailRow(
              theme,
              title: '分类',
              value: task.subject,
              color: task.color,
            ),
            _buildDetailRow(
              theme,
              title: '状态',
              value: task.done ? '已完成' : '未完成',
              color: task.color,
            ),
            _buildDetailRow(
              theme,
              title: '说明',
              value: task.description,
              color: task.color,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildDetailRow(
    ThemeData theme, {
    required String title,
    required String value,
    required Color color,
  }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            width: 7,
            height: 7,
            margin: const EdgeInsets.only(top: 8),
            decoration: BoxDecoration(
              color: color,
              shape: BoxShape.circle,
            ),
          ),
          const SizedBox(width: 10),
          SizedBox(
            width: 58,
            child: Text(
              title,
              style: theme.textTheme.bodyMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          Expanded(
            child: Text(
              value,
              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: Row(
          children: [
            Expanded(
              child: ElevatedButton.icon(
                onPressed: _finishSelectedTask,
                icon: const Icon(Icons.check_circle),
                label: const Text('完成当前'),
              ),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: OutlinedButton.icon(
                onPressed: _restartTextAnimation,
                icon: const Icon(Icons.replay),
                label: const Text('重播文字'),
              ),
            ),
          ],
        ),
      ),
    );
  }

  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),
            _buildLibraryInfoRow(
              theme,
              title: '库名称',
              value: 'animated_text_kit',
            ),
            _buildLibraryInfoRow(
              theme,
              title: '配置文件',
              value: 'pubspec.yaml',
            ),
            _buildLibraryInfoRow(
              theme,
              title: '导入方式',
              value:
                  "import 'package:animated_text_kit/animated_text_kit.dart';",
            ),
            _buildLibraryInfoRow(
              theme,
              title: '核心组件',
              value:
                  'AnimatedTextKit / TypewriterAnimatedText / ColorizeAnimatedText',
            ),
            _buildLibraryInfoRow(
              theme,
              title: '核心能力',
              value: '打字机文字、淡入淡出文字、彩色文字、波浪文字',
            ),
            _buildLibraryInfoRow(
              theme,
              title: '应用场景',
              value: '启动页、首页标语、学习提醒、任务提示、活动标题',
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildLibraryInfoRow(
    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,
                height: 1.5,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

九、代码实现说明

1. 引入 animated_text_kit 第三方库

代码开头引入第三方库:

import 'package:animated_text_kit/animated_text_kit.dart';

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

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

AnimatedTextKit
TypewriterAnimatedText
FadeAnimatedText
ColorizeAnimatedText
WavyAnimatedText
TyperAnimatedText

其中:

组件 作用
AnimatedTextKit 文字动画容器
TypewriterAnimatedText 打字机文字动画
FadeAnimatedText 淡入淡出文字动画
ColorizeAnimatedText 彩色渐变文字动画
WavyAnimatedText 波浪文字动画
TyperAnimatedText 逐字显示文字动画

2. 使用 AnimatedTextKit 构建动态标题

页面顶部动态标题代码如下:

AnimatedTextKit(
  repeatForever: true,
  pause: const Duration(milliseconds: 900),
  displayFullTextOnTap: true,
  stopPauseOnTap: true,
  animatedTexts: [
    TypewriterAnimatedText(...),
    FadeAnimatedText(...),
    ColorizeAnimatedText(...),
  ],
)

参数说明如下:

参数 作用
repeatForever 是否一直循环播放
pause 每段文字之间的停顿时间
displayFullTextOnTap 点击时直接显示完整文字
stopPauseOnTap 点击时跳过暂停
animatedTexts 需要播放的文字动画列表

这部分是项目中第三方库使用最明显的位置。


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

打字机文字代码如下:

TypewriterAnimatedText(
  '今天先完成一个小目标',
  textStyle: titleStyle,
  speed: const Duration(milliseconds: 80),
)

其中:

参数 作用
文本内容 要显示的文字
textStyle 文字样式
speed 每个字符显示速度

这个效果适合用于欢迎语、学习提醒和提示语。


4. 使用 FadeAnimatedText 实现淡入淡出效果

淡入淡出文字代码如下:

FadeAnimatedText(
  '学习不靠玄学,靠重复',
  textStyle: titleStyle,
  textAlign: TextAlign.center,
)

FadeAnimatedText 可以让文字以淡入淡出的方式切换,适合用于页面提示语或者状态说明。

这种效果比普通文字更有提示感,也不会像弹窗一样打扰用户。终于有一种 UI 效果不是靠吓人来获得注意力了。


5. 使用 ColorizeAnimatedText 实现彩色文字效果

彩色文字代码如下:

ColorizeAnimatedText(
  'Flutter for OpenHarmony',
  textStyle: _colorizeTextStyle,
  colors: _colorizeColors,
)

颜色列表如下:

static const List<Color> _colorizeColors = [
  Colors.indigo,
  Colors.purple,
  Colors.blue,
  Colors.teal,
];

ColorizeAnimatedText 会让文字在多个颜色之间变化,适合用于页面标题和重点标语。


6. 使用 WavyAnimatedText 实现空状态提示

当没有未完成任务时,页面会显示波浪文字:

WavyAnimatedText(
  '没有未完成任务',
  textStyle: theme.textTheme.titleMedium!.copyWith(
    color: theme.colorScheme.primary,
    fontWeight: FontWeight.bold,
  ),
)

这个效果用于空状态提示,比普通静态文字更明显。终于不是一行冷冰冰的“暂无数据”了,UI 也该有点尊严。


7. 使用 TyperAnimatedText 实现任务详情提示

任务详情区域使用了 TyperAnimatedText

TyperAnimatedText(
  task.done ? '这个任务已经完成' : '这个任务还需要继续推进',
  textStyle: theme.textTheme.titleMedium!.copyWith(
    color: task.color,
    fontWeight: FontWeight.bold,
  ),
  speed: const Duration(milliseconds: 70),
)

当用户点击不同任务或者修改任务状态时,详情区域会显示对应的动态提示。

如果任务已经完成,显示“这个任务已经完成”。

如果任务未完成,显示“这个任务还需要继续推进”。

这样可以让任务状态更加直观。


8. 使用任务模型管理数据

项目中定义了学习任务模型:

class StudyTask {
  StudyTask({
    required this.id,
    required this.title,
    required this.subject,
    required this.description,
    required this.minutes,
    required this.icon,
    required this.color,
    this.done = false,
  });

  final int id;
  final String title;
  final String subject;
  final String description;
  final int minutes;
  final IconData icon;
  final Color color;
  bool done;
}

字段说明如下:

字段 作用
id 任务编号
title 任务标题
subject 学习分类
description 任务说明
minutes 预计耗时
icon 任务图标
color 任务主题色
done 是否完成

其中 done 是可变化状态,用于实现任务勾选。


9. 实现任务完成状态切换

任务完成状态切换方法如下:

void _toggleTask(StudyTask task) {
  setState(() {
    task.done = !task.done;
    _selectedTaskId = task.id;
  });
}

点击任务右侧的复选框时,会切换当前任务的完成状态。

页面中的进度条、已完成数量、未完成数量和任务详情都会同步更新。


10. 实现未完成任务筛选

筛选逻辑如下:

List<StudyTask> get _visibleTasks {
  if (_showOnlyTodo) {
    return _tasks.where((task) => !task.done).toList();
  }

  return _tasks;
}

如果打开“只显示未完成任务”,页面只展示 done == false 的任务。

如果关闭筛选,则展示全部任务。


11. 实现当前任务详情

点击任务列表中的某一项时,会执行:

void _selectTask(StudyTask task) {
  setState(() {
    _selectedTaskId = task.id;
  });
}

当前选中任务详情会展示:

  • 任务标题;
  • 学习分类;
  • 完成状态;
  • 任务说明;
  • 动态文字提示。

详情区域也使用了 TyperAnimatedText,用于显示当前任务状态提示。


12. 重新播放动态文字

项目中使用 _textSeed 控制动态文字重新构建:

int _textSeed = 0;

点击“重播文字”按钮时:

void _restartTextAnimation() {
  setState(() {
    _textSeed++;
  });
}

顶部 AnimatedTextKit 使用:

key: ValueKey(_textSeed),

当 key 改变时,动态文字组件会重新构建,从而重新播放动画。


十、运行项目

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

flutter pub get

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

查看设备:

flutter devices

运行项目:

flutter run

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

运行成功后,页面会显示“学习计划动态标语”。用户可以看到打字机标题、彩色文字动画、任务列表、学习进度统计和当前任务详情。


十一、开发中遇到的问题

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 是否设置了合适颜色;
  • 外层是否给了足够高度;
  • 项目是否成功运行。

本项目中使用了:

SizedBox(
  height: 64,
  child: Center(
    child: AnimatedTextKit(...),
  ),
)

这样可以避免动态文字区域高度不足导致显示异常。


4. ColorizeAnimatedText 报错

ColorizeAnimatedText 需要提供颜色列表:

colors: _colorizeColors

颜色列表至少要有两个颜色。如果只给一个颜色,就失去了彩色变化的意义,组件也可能无法正常表现。


5. 动态文字切换后没有重新播放

如果希望动态文字重新播放,可以给 AnimatedTextKit 设置一个会变化的 key:

key: ValueKey(_textSeed)

_textSeed 改变时,组件会重新构建,动画也会重新开始。


6. 任务勾选后页面没有变化

如果点击复选框后页面没有刷新,需要检查状态修改是否放在 setState() 中:

setState(() {
  task.done = !task.done;
});

Flutter 页面不会自己猜数据变化。它是框架,不是占卜摊。


7. WavyAnimatedText 没有触发

如果筛选后没有看到 WavyAnimatedText,可以先把所有任务都勾选完成,再打开“只显示未完成任务”。

当未完成任务列表为空时,页面才会进入空状态:

if (visibleTasks.isEmpty)

这时才会显示:

WavyAnimatedText('没有未完成任务')

所以不是组件失效,只是触发条件还没满足。条件判断这种东西很冷酷,它不会因为你期待它显示就显示。


8. 运行不到 OpenHarmony 设备

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

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

如果 flutter devices 都识别不到设备,那应该先处理环境问题,而不是怀疑 animated_text_kit。文字动画没那么大本事把设备藏起来。


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

本文是 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 实现彩色文字动画;
  • 使用 WavyAnimatedText 实现空状态提示;
  • 使用 TyperAnimatedText 实现任务详情动态提示;
  • 使用数据模型管理学习任务;
  • 使用 setState() 实现任务完成状态更新;
  • 使用 Flutter Material 组件构建完整页面;
  • 将项目运行到 OpenHarmony 设备或模拟器中。

这个项目虽然只是一个基础学习计划页面,但完整展示了 Flutter for OpenHarmony 项目中第三方库的使用流程。

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

  • 添加任务新增功能;
  • 添加任务删除功能;
  • 添加任务编辑功能;
  • 添加学习分类管理;
  • 添加本地数据保存;
  • 添加每日打卡记录;
  • 添加学习时长统计;
  • 添加暗色主题;
  • 添加更多文字动画类型;
  • 添加页面启动动画;
  • 添加学习提醒通知。

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

Logo

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

更多推荐