Amal使用Rive(原名Flare)完成所有角色动画——包括口型同步的语音、头像定制、反馈反应和游戏角色动画。我们选择Rive而非Lottie或精灵图,因为它支持运行时状态机、程序化控制和GPU加速渲染,保证每个角色文件小巧且能以60fps流畅播放。
动画资源库
核心角色
lip-sync-amal-01.riv
- 主要Amal角色(全身及仅面部变体)
- 每个嘴型位置含多个画板(用于音素映射)
- 状态:静止、说话、错误、庆祝、睡觉
- 文件大小:1.2 MB(远小于50+ MB的精灵图)avatar.riv
- 可定制用户头像(3个画板)
1. 全身含头部、躯干、四肢及服饰
2. 仅头部:用于仪表盘和家长App
3. 蝴蝶伙伴:奖励动画
- 组件化:头型、发型、眼睛、衣服、配饰、颜色
- 文件大小:2.4 MBcoin-01.riv&coins-01.riv
- 奖励动画(硬币漂浮与收集)
- 单个硬币:150 KB
- 多个硬币:300 KBcute-monster-final.riv
- 带多种情绪状态的反馈角色
- 状态:高兴(回答正确)、困惑(错误)、思考(处理中)、庆祝(连胜)
- 文件大小:1.8 MB
Android专属优化
- 定制NDK构建(Rive NDK-r28),满足16KB页对齐要求
- 相比标准版本减小二进制体积8%
- 保证Android 12及以上内存管理兼容
口型同步工作流程(技术深入)
步骤1:生成TTS音频和获取语音标记
from google.cloud import texttospeech
def generate_speech_with_marks(text: str, language: str = 'ar-SA'):
client = texttospeech.TextToSpeechClient()
synthesis_input = texttospeech.SynthesisInput(text=text)
voice = texttospeech.VoiceSelectionParams(
language_code=language,
name='ar-SA-Neural2-A' # WaveNet声音
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3,
effects_profile_id=['small-bluetooth-speaker-class-device'] # 儿童音箱
)
request = texttospeech.SynthesizeSpeechRequest(
input=synthesis_input,
voice=voice,
audio_config=audio_config,
enable_text_to_speech_as_cloud_service=True
)
response = client.synthesize_speech(request=request)
return {
'audio': response.audio_content,
'speech_marks': response.timepoints # 音素级时间戳
}
步骤2:将音素映射到Rive嘴型状态
{
"phoneme_map": {
"ا": { "rive_state": "mouth_a_open", "duration_ms": 200 },
"ب": { "rive_state": "mouth_lips_closed", "duration_ms": 150 },
"ع": { "rive_state": "mouth_pharyngeal", "duration_ms": 250 },
"ق": { "rive_state": "mouth_uvular", "duration_ms": 180 },
...
},
"mouth_positions": [
{ "id": "mouth_a_open", "blend_values": { "jaw_open": 0.8, "lips_rounded": 0.2 } },
{ "id": "mouth_lips_closed", "blend_values": { "jaw_open": 0.1, "lips_rounded": 0.9 } },
...
]
}
步骤3:LipSyncController控制播放
class LipSyncController extends GetxController {
late Rive riveCharacter;
late AudioPlayer audioPlayer;
late LipSyncMapper mapper;
void playWithLipSync(String text, String audioPath) {
riveCharacter.loadRiveFile('lip-sync-amal-01.riv');
mapper = LipSyncMapper.fromJson(loadJsonAsset('lip_sync_avatar.json'));
audioPlayer.play(AudioSource.file(audioPath));
audioPlayer.onPositionChanged.listen((Duration position) {
String phoneme = mapper.phonemeAtTime(position.inMilliseconds);
String riveState = mapper.riveStateForPhoneme(phoneme);
riveCharacter.setStateInput('mouth_state', riveState);
});
}
}
步骤4:RiveCharacterController管理角色生命周期
class RiveCharacterController extends GetxController {
// 状态机: idle → prepare → speaking → idle → error/celebration
void startExercise() {
character.setStateInput('state_machine', 'prepare');
}
void childSpeaks(String recognizedText, double accuracy) {
character.setStateInput('state_machine', 'speaking');
lipSyncController.playFeedback(recognizedText);
}
void onFeedbackComplete(bool wasCorrect) {
if (wasCorrect) {
character.setStateInput('state_machine', 'celebrate');
playRewardAnimation();
} else {
character.setStateInput('state_machine', 'error');
playEncouragingPhrase();
}
}
}
头像定制系统
组件化架构
孩子们可从多个部件定制头像:
{
"avatar_customization": {
"head_shapes": [
{ "id": "round", "rive_element": "head_round" },
{ "id": "oval", "rive_element": "head_oval" },
{ "id": "square", "rive_element": "head_square" }
],
"hair_styles": [
{ "id": "ponytail", "rive_element": "hair_ponytail" },
{ "id": "braids", "rive_element": "hair_braids" },
{ "id": "straight", "rive_element": "hair_straight" }
],
"colors": {
"skin_tone": ["light", "medium", "dark"],
"hair_color": ["black", "brown", "blonde", "red"],
"shirt_color": ["blue", "pink", "green", "yellow", "purple"],
"accent_color": ["red", "orange", "green", "blue"]
}
}
}
Rive元素名称映射 (avatar_customization_rive_names.dart)
const avatarRiveNames = {
'head_round': 'Head_Round',
'head_oval': 'Head_Oval',
'hair_ponytail': 'Hair_Ponytail',
'shirt_blue': 'Shirt_Blue',
'shirt_pink': 'Shirt_Pink',
// ... 50+元素映射
};
孩子选择“圆形头 + 蓝色衬衫”时,App会启用Head_Round和Shirt_Blue,禁用其他所有头型和颜色,确保个性化头像贯穿整个应用。
为何选择Rive而非其他方案
| 功能 | Rive | Lottie | 精灵图 | 视频 |
|---|---|---|---|---|
| 状态机 | ✓ | ✗ | ✗ | ✗ |
| 运行时控制 | 完全支持 | 部分 | 手动 | 无 |
| 文件大小 | 1-2 MB | 2-3 MB | 50+ MB | 100+ MB |
| 性能 | 60fps GPU | 30fps CPU | 60fps GPU | 不定 |
| 交互性 | 全面 | 部分 | 全面 | 无 |
| 学习曲线 | 适中 | 简单 | 简单 | 简单 |
| 维护 | 单个.riv文件 | 单个JSON文件 | 数百图像 | 单个视频 |
Rive胜出,因为我们需要程序控制、状态机和文件紧凑,适合移动应用需求。
性能优化
- 预加载角色:启动时加载.riv文件,避免每次练习重复加载
- GPU渲染:检测设备支持时自动使用GPU,旧设备则使用CPU回退
- 内存池机制:复用Rive控制器,减少GC暂停
- 压缩:Rive文件已压缩,无需额外优化
效果是在高通骁龙662及以上中端手机上实现60fps流畅动画。
常见问答
问:能否从Adobe Animate导出动画到Rive?
答:不能直接导出。我们的动画师使用Rive官方编辑器(rive.app)设计动画,流程是:在Rive中设计角色 → 导出.riv文件 → 集成到Flutter App。
问:如何处理不同体型或残障需求?
答:头像定制系统包含体型选项(瘦、运动型、圆润)及辅助配件(眼镜、助听器、辅助设备),确保所有孩子都能看到自我代表。
问:如果孩子不喜欢头像怎么办?
答:孩子随时可以修改。App不会强制头像样式,完全尊重儿童的创意和选择权。



