前言 🌟
大家好!今天我要给大家分享如何在 Flutter 中实现一个酷炫的 AI Bot 动画效果 🤖✨。这份代码灵感来自掘金的juejin.cn/post/750720… 的css动画,通过 Flutter 的 CustomPainter 和 AnimationController,我们可以将静态的图形赋予生命,让小机器人栩栩如生地动起来!
本文会从整体结构、动画控制、绘制逻辑等多个模块入手,逐段代码进行详细解读,希望对你学习 Flutter 动画和自定义绘制有所帮助~
import 'package:flutter/material.dart';
import 'dart:math' as math;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AI Bot Animation',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF111111), // --surface color
body: Center(
child: Transform.scale(
scale: 4.2, // Match the scale from CSS
child: const AIBot(),
),
),
);
}
}
class AIBot extends StatefulWidget {
const AIBot({super.key});
@override
State<AIBot> createState() => _AIBotState();
}
/// AI Bot 动画状态管理类
class _AIBotState extends State<AIBot> with TickerProviderStateMixin {
// 头部动画控制器和动画
late AnimationController _headController;
late Animation<double> _headAnimation;
// 眼睛动画控制器和动画
late AnimationController _eyesController;
late Animation<double> _eyesAnimation;
// 嘴巴动画控制器和动画(Y轴和X轴缩放)
late AnimationController _mouthController;
late Animation<double> _mouthScaleYAnimation;
late Animation<double> _mouthScaleXAnimation;
@override
void initState() {
super.initState();
/// 初始化头部动画
/// - 动画周期:4200ms
/// - 动画序列:
/// - 20% 时间保持静止
/// - 10% 时间向右倾斜(0 → 0.2)
/// - 10% 时间恢复到中心位置(0.2 → 0)
/// - 10% 时间向左倾斜(0 → -0.2)
/// - 10% 时间恢复到中心位置(-0.2 → 0)
/// - 40% 时间保持静止
_headController = AnimationController(
duration: const Duration(milliseconds: 4200),
vsync: this,
)..repeat();
_headAnimation = TweenSequence<double>([
TweenSequenceItem(tween: Tween<double>(begin: 0, end: 0), weight: 20),
TweenSequenceItem(tween: Tween<double>(begin: 0, end: 0.2), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: 0.2, end: 0), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: 0, end: -0.2), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: -0.2, end: 0), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: 0, end: 0), weight: 40),
]).animate(_headController);
/// 初始化眼睛动画
/// - 动画周期:2400ms
/// - 动画序列:
/// - 10% 时间保持完全睁开
/// - 2% 时间快速闭合(1 → 0.2)
/// - 6% 时间完全闭合(0.2 → 0.1)
/// - 2% 时间快速睁开(0.1 → 1)
/// - 80% 时间保持完全睁开
_eyesController = AnimationController(
duration: const Duration(milliseconds: 2400),
vsync: this,
)..repeat();
_eyesAnimation = TweenSequence<double>([
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 1), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 0.2), weight: 2),
TweenSequenceItem(tween: Tween<double>(begin: 0.2, end: 0.1), weight: 6),
TweenSequenceItem(tween: Tween<double>(begin: 0.1, end: 1), weight: 2),
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 1), weight: 80),
]).animate(_eyesController);
/// 初始化嘴巴动画
/// - 动画周期:1200ms
/// - Y轴缩放动画序列:
/// - 30% 时间保持正常高度
/// - 20% 时间压缩高度(1 → 0.5)
/// - 20% 时间恢复高度(0.5 → 1)
/// - 30% 时间保持正常高度
/// - X轴缩放动画序列:
/// - 60% 时间保持正常宽度
/// - 10% 时间缩小宽度(1 → 0.7)
/// - 10% 时间恢复宽度(0.7 → 1)
/// - 20% 时间保持正常宽度
_mouthController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
)..repeat();
_mouthScaleYAnimation = TweenSequence<double>([
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 1), weight: 30),
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 0.5), weight: 20),
TweenSequenceItem(tween: Tween<double>(begin: 0.5, end: 1), weight: 20),
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 1), weight: 30),
]).animate(_mouthController);
_mouthScaleXAnimation = TweenSequence<double>([
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 1), weight: 60),
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 0.7), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: 0.7, end: 1), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 1), weight: 20),
]).animate(_mouthController);
}
@override
void dispose() {
/// 释放动画控制器资源,防止内存泄漏
_headController.dispose();
_eyesController.dispose();
_mouthController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
/// 构建动画组件
/// - 监听多个动画控制器的变化
/// - 使用 `Transform.translate` 实现头部移动效果
/// - 使用 `CustomPaint` 绘制机器人面部特征
return AnimatedBuilder(
animation: Listenable.merge([_headController, _eyesController, _mouthController]),
builder: (context, child) {
return SizedBox(
width: 34,
height: 34,
child: Center(
child: Transform.translate(
offset: Offset(_headAnimation.value * 6.8, 0), // 根据头部动画值平移
child: CustomPaint(
size: const Size(28, 20),
painter: AIBotPainter(
eyesScaleY: _eyesAnimation.value,
mouthScaleY: _mouthScaleYAnimation.value,
mouthScaleX: _mouthScaleXAnimation.value,
),
),
),
),
);
},
);
}
}
/// 自定义绘制类,用于绘制AI Bot的外观。
///
/// 该类继承自 `CustomPainter`,通过 `paint` 方法在画布上绘制AI Bot的头部、天线、眼睛和嘴巴。
/// 可以通过调整 `eyesScaleY`、`mouthScaleY` 和 `mouthScaleX` 参数来动态改变眼睛和嘴巴的大小。
class AIBotPainter extends CustomPainter {
/// 眼睛垂直方向的缩放比例。
final double eyesScaleY;
/// 嘴巴垂直方向的缩放比例。
final double mouthScaleY;
/// 嘴巴水平方向的缩放比例。
final double mouthScaleX;
/// 构造函数,接收眼睛和嘴巴的缩放参数。
AIBotPainter({
required this.eyesScaleY,
required this.mouthScaleY,
required this.mouthScaleX,
});
@override
void paint(Canvas canvas, Size size) {
// 定义绘制颜色
final white = Paint()..color = Colors.white; // 白色画笔
final surface = Paint()..color = const Color(0xFF111111); // 背景色画笔
final cyan = Paint()..color = const Color(0xFF9ae3dc); // 青色画笔
final magenta = Paint()..color = Color(0xFFD73BE9); // 洋红色画笔
// 绘制头部
final headRect = Rect.fromLTWH(0, 0, size.width, size.height); // 头部矩形区域
final headRRect = RRect.fromRectAndRadius(headRect, const Radius.circular(6)); // 圆角矩形头部
// 头部渐变效果
final headGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white,
Colors.white,
Colors.white,
Colors.white,
Colors.white.withOpacity(0.7),
Colors.white,
],
stops: const [0.0, 0.7, 0.8, 0.85, 0.9, 1.0],
);
final headPaint = Paint()..shader = headGradient.createShader(headRect); // 应用渐变
canvas.drawRRect(headRRect, headPaint); // 绘制头部
// 绘制天线
final leftAntenna = RRect.fromRectAndRadius(
Rect.fromLTWH(-4, 6, 2, 8), // 左侧天线位置
const Radius.circular(2),
);
canvas.drawRRect(leftAntenna, white); // 绘制左侧天线
final rightAntenna = RRect.fromRectAndRadius(
Rect.fromLTWH(size.width + 2, 6, 2, 8), // 右侧天线位置
const Radius.circular(2),
);
canvas.drawRRect(rightAntenna, white); // 绘制右侧天线
// 绘制脸部背景
final faceRect = Rect.fromLTWH(3, 0, size.width - 6, size.height); // 脸部矩形区域
final faceRRect = RRect.fromRectAndRadius(faceRect, const Radius.circular(4)); // 圆角矩形脸部
canvas.drawRRect(faceRRect, surface); // 绘制脸部背景
// 绘制顶部凹槽
final notchPaint = Paint()..color = Colors.white; // 凹槽颜色
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(size.width / 2 - 5, -2, 10, 1), // 凹槽位置
const Radius.circular(1),
),
notchPaint,
);
// 绘制眼睛
final eyeWidth = 5.0; // 眼睛宽度
final eyeHeight = 8.0 * eyesScaleY; // 眼睛高度(根据缩放比例调整)
final eyeSpacing = 6.0; // 眼睛间距
final eyeY = 4.0; // 眼睛垂直位置
// 左眼
final leftEyeRect = Rect.fromLTWH(
size.width / 2 - eyeWidth - eyeSpacing / 2,
eyeY + (8 - eyeHeight) / 2,
eyeWidth,
eyeHeight,
);
// 右眼
final rightEyeRect = Rect.fromLTWH(
size.width / 2 + eyeSpacing / 2,
eyeY + (8 - eyeHeight) / 2,
eyeWidth,
eyeHeight,
);
// 眼睛发光效果
final eyeGlowPaint = Paint()
..color = cyan.color
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
canvas.drawRRect(
RRect.fromRectAndRadius(leftEyeRect, const Radius.circular(1)),
eyeGlowPaint,
);
canvas.drawRRect(
RRect.fromRectAndRadius(rightEyeRect, const Radius.circular(1)),
eyeGlowPaint,
);
// 眼睛渐变效果
final eyeGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [magenta.color, Colors.transparent],
stops: const [0.0, 0.6],
);
final eyeGradientPaint = Paint()..shader = eyeGradient.createShader(leftEyeRect);
// 绘制眼睛及渐变效果
canvas.drawRRect(
RRect.fromRectAndRadius(leftEyeRect, const Radius.circular(1)),
cyan,
);
canvas.drawRRect(
RRect.fromRectAndRadius(leftEyeRect, const Radius.circular(1)),
eyeGradientPaint,
);
canvas.drawRRect(
RRect.fromRectAndRadius(rightEyeRect, const Radius.circular(1)),
cyan,
);
canvas.drawRRect(
RRect.fromRectAndRadius(rightEyeRect, const Radius.circular(1)),
eyeGradientPaint,
);
// 绘制嘴巴
final mouthWidth = 10.0 * mouthScaleX; // 嘴巴宽度(根据缩放比例调整)
final mouthHeight = 2.0 * mouthScaleY; // 嘴巴高度(根据缩放比例调整)
final mouthRect = Rect.fromLTWH(
size.width / 2 - mouthWidth / 2,
15, // 固定垂直位置
mouthWidth,
mouthHeight,
);
// 嘴巴发光效果
final mouthGlowPaint = Paint()
..color = cyan.color
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
canvas.drawRRect(
RRect.fromRectAndCorners(
mouthRect,
bottomLeft: const Radius.circular(1),
bottomRight: const Radius.circular(1),
),
mouthGlowPaint,
);
canvas.drawRRect(
RRect.fromRectAndCorners(
mouthRect,
bottomLeft: const Radius.circular(1),
bottomRight: const Radius.circular(1),
),
cyan,
);
}
@override
bool shouldRepaint(AIBotPainter oldDelegate) {
/// 判断是否需要重新绘制。
/// 当 `eyesScaleY`、`mouthScaleY` 或 `mouthScaleX` 发生变化时,返回 `true`。
return oldDelegate.eyesScaleY != eyesScaleY ||
oldDelegate.mouthScaleY != mouthScaleY ||
oldDelegate.mouthScaleX != mouthScaleX;
}
}
1. 应用入口与整体布局 🚀
import 'package:flutter/material.dart';
import 'dart:math' as math;
void main() {
runApp(const MyApp());
}
这一段是标准的 Flutter 应用入口,调用 runApp 启动我们的 MyApp。🌱
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AI Bot Animation',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
在 MyApp 中,我们使用 MaterialApp 包裹,设置了主题色和去除调试标志位,方便我们专注于动画效果~
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF111111), // --surface color
body: Center(
child: Transform.scale(
scale: 4.2, // Match the scale from CSS
child: const AIBot(),
),
),
);
}
}
MyHomePage 是我们的主页,使用深色背景,将 AIBot 放在屏幕中央,并通过 Transform.scale 放大到合适尺寸 🔍。
2. 动画状态管理 🕹️
class AIBot extends StatefulWidget {
const AIBot({super.key});
@override
State<AIBot> createState() => _AIBotState();
}
class _AIBotState extends State<AIBot> with TickerProviderStateMixin {
late AnimationController _headController;
late Animation<double> _headAnimation;
late AnimationController _eyesController;
late Animation<double> _eyesAnimation;
late AnimationController _mouthController;
late Animation<double> _mouthScaleYAnimation;
late Animation<double> _mouthScaleXAnimation;
@override
void initState() {
super.initState();
// Head movement animation
_headController = AnimationController(
duration: const Duration(milliseconds: 4200),
vsync: this,
)..repeat();
_headAnimation = TweenSequence<double>([
TweenSequenceItem(tween: Tween<double>(begin: 0, end: 0), weight: 20),
TweenSequenceItem(tween: Tween<double>(begin: 0, end: 0.2), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: 0.2, end: 0), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: 0, end: -0.2), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: -0.2, end: 0), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: 0, end: 0), weight: 40),
]).animate(_headController);
// Eyes blinking animation
_eyesController = AnimationController(
duration: const Duration(milliseconds: 2400),
vsync: this,
)..repeat();
_eyesAnimation = TweenSequence<double>([
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 1), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 0.2), weight: 2),
TweenSequenceItem(tween: Tween<double>(begin: 0.2, end: 0.1), weight: 6),
TweenSequenceItem(tween: Tween<double>(begin: 0.1, end: 1), weight: 2),
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 1), weight: 80),
]).animate(_eyesController);
// Mouth animation
_mouthController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
)..repeat();
_mouthScaleYAnimation = TweenSequence<double>([
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 1), weight: 30),
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 0.5), weight: 20),
TweenSequenceItem(tween: Tween<double>(begin: 0.5, end: 1), weight: 20),
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 1), weight: 30),
]).animate(_mouthController);
_mouthScaleXAnimation = TweenSequence<double>([
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 1), weight: 60),
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 0.7), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: 0.7, end: 1), weight: 10),
TweenSequenceItem(tween: Tween<double>(begin: 1, end: 1), weight: 20),
]).animate(_mouthController);
}
@override
void dispose() {
_headController.dispose();
_eyesController.dispose();
_mouthController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: Listenable.merge([_headController, _eyesController, _mouthController]),
builder: (context, child) {
return SizedBox(
width: 34,
height: 34,
child: Center(
child: Transform.translate(
offset: Offset(_headAnimation.value * 6.8, 0), // Translate based on head animation
child: CustomPaint(
size: const Size(28, 20),
painter: AIBotPainter(
eyesScaleY: _eyesAnimation.value,
mouthScaleY: _mouthScaleYAnimation.value,
mouthScaleX: _mouthScaleXAnimation.value,
),
),
),
),
);
},
);
}
}
这一大段代码负责:
- 创建三个
AnimationController,分别控制头部摇摆、眼睛眨眼和嘴巴张合 🎬。 - 通过
TweenSequence精细规划每个动画阶段的时长和插值效果,让动作更加自然。 - 在
build方法中使用AnimatedBuilder合并监听三个控制器,根据当前动画值来平移头部和刷新画布。
3. 自定义绘制逻辑 🎨
class AIBotPainter extends CustomPainter {
final double eyesScaleY;
final double mouthScaleY;
final double mouthScaleX;
AIBotPainter({
required this.eyesScaleY,
required this.mouthScaleY,
required this.mouthScaleX,
});
@override
void paint(Canvas canvas, Size size) {
// 1️⃣ 头部背景与渐变高光
final Rect headRect = Rect.fromLTWH(0, 0, size.width, size.height);
final RRect headRRect = RRect.fromRectAndRadius(
headRect, const Radius.circular(6),
);
// 使用线性渐变模拟光泽
final headGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white,
Colors.white,
Colors.white.withOpacity(0.7),
Colors.white,
],
stops: [0.0, 0.7, 0.9, 1.0],
);
final Paint headPaint = Paint()..shader = headGradient.createShader(headRect);
canvas.drawRRect(headRRect, headPaint);
// 2️⃣ 天线和顶部凹槽
final Paint antennaPaint = Paint()..color = Colors.white;
// 左天线
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(-4, 6, 2, 8), Radius.circular(2),
),
antennaPaint,
);
// 右天线
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(size.width + 2, 6, 2, 8), Radius.circular(2),
),
antennaPaint,
);
// 顶部小凹槽
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(size.width / 2 - 5, -2, 10, 1), Radius.circular(1),
),
antennaPaint,
);
// 3️⃣ 面板底色
final Paint surfacePaint = Paint()..color = const Color(0xFF111111);
final RRect faceRRect = RRect.fromRectAndRadius(
Rect.fromLTWH(3, 0, size.width - 6, size.height), Radius.circular(4),
);
canvas.drawRRect(faceRRect, surfacePaint);
// 4️⃣ 眼睛绘制(眨眼 & 发光)
final double eyeWidth = 5.0;
final double eyeHeight = 8.0 * eyesScaleY; // 根据 eyesScaleY 动态缩放
final double eyeSpacing = 6.0;
final double eyeY = 4.0 + (8 - eyeHeight) / 2; // 垂直居中
final Rect leftEye = Rect.fromLTWH(
size.width / 2 - eyeSpacing / 2 - eyeWidth,
eyeY,
eyeWidth,
eyeHeight,
);
final Rect rightEye = Rect.fromLTWH(
size.width / 2 + eyeSpacing / 2,
eyeY,
eyeWidth,
eyeHeight,
);
// 眨眼发光效果
final Paint glowPaint = Paint()
..color = const Color(0xFF9ae3dc)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
canvas.drawRRect(RRect.fromRectAndRadius(leftEye, const Radius.circular(1)), glowPaint);
canvas.drawRRect(RRect.fromRectAndRadius(rightEye, const Radius.circular(1)), glowPaint);
// 眼睛主体渐变
final Paint eyePaint = Paint()..color = const Color(0xFF9ae3dc);
canvas.drawRRect(RRect.fromRectAndRadius(leftEye, const Radius.circular(1)), eyePaint);
canvas.drawRRect(RRect.fromRectAndRadius(rightEye, const Radius.circular(1)), eyePaint);
// 叠加线条纹理
final Paint linePaint = Paint()
..color = Colors.white
..strokeWidth = 0.25;
for (double y = 0; y < eyeHeight; y += 0.85) {
canvas.drawLine(
Offset(leftEye.left, leftEye.top + y),
Offset(leftEye.right, leftEye.top + y),
linePaint,
);
canvas.drawLine(
Offset(rightEye.left, rightEye.top + y),
Offset(rightEye.right, rightEye.top + y),
linePaint,
);
}
// 5️⃣ 嘴巴绘制(张合 & 发光)
final double mouthWidth = 10.0 * mouthScaleX;
final double mouthHeight = 2.0 * mouthScaleY;
final Rect mouthRect = Rect.fromLTWH(
size.width / 2 - mouthWidth / 2,
15,
mouthWidth,
mouthHeight,
);
final Paint mouthGlow = Paint()
..color = const Color(0xFF9ae3dc)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
canvas.drawRRect(
RRect.fromRectAndCorners(
mouthRect,
bottomLeft: const Radius.circular(1),
bottomRight: const Radius.circular(1),
),
mouthGlow,
);
final Paint mouthPaint = Paint()..color = const Color(0xFF9ae3dc);
canvas.drawRRect(
RRect.fromRectAndCorners(
mouthRect,
bottomLeft: const Radius.circular(1),
bottomRight: const Radius.circular(1),
),
mouthPaint,
);
}
@override
bool shouldRepaint(AIBotPainter oldDelegate) {
return oldDelegate.eyesScaleY != eyesScaleY ||
oldDelegate.mouthScaleY != mouthScaleY ||
oldDelegate.mouthScaleX != mouthScaleX;
}
}
✨ 详解
- 头部背景:先通过
RRect和LinearGradient绘制一个带高光渐变的圆角矩形,模拟金属质感。 - 天线 & 凹槽:用两个小圆角矩形画出天线,再在顶部中心绘制一个细长凹槽,增加科幻感。
- 面板底色:在头部内部绘制深色矩形,作为机器人“面板”的底色。
- 眼睛:在两侧绘制可缩放的圆角矩形,并叠加发光和细线条,让眨眼效果更柔和且富有层次感。
- 嘴巴:通过缩放
Rect实现张合动作,并配合朦胧发光,营造出“说话中”的动态效果。
4. 效果演示及扩展建议 💡
最终效果如图所示,小 AI 机器人会左右摇头、眨眼、张嘴,非常生动可爱!
- 扩展思路:可以接入语音识别,让机器人根据声音节奏摇头和说话~
- 性能优化:对于复杂渐变和模糊效果,可以考虑提前缓存为图像,减少每帧绘制成本。
结语 🙏
以上就是本次 Flutter AI Bot 动画的完整源码解析与思路拆解,希望能给大家带来灵感🔥。如果你有任何问题或更酷的玩法,欢迎在评论区交流~