用Flutter打造一个超萌的AI机器人动画!🤖✨

448 阅读9分钟

前言 🌟

大家好!今天我要给大家分享如何在 Flutter 中实现一个酷炫的 AI Bot 动画效果 🤖✨。这份代码灵感来自掘金的juejin.cn/post/750720… 的css动画,通过 Flutter 的 CustomPainterAnimationController,我们可以将静态的图形赋予生命,让小机器人栩栩如生地动起来!

本文会从整体结构、动画控制、绘制逻辑等多个模块入手,逐段代码进行详细解读,希望对你学习 Flutter 动画和自定义绘制有所帮助~

QQ2025523-215233 00_00_00-00_00_30.gif


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;
  }
}

详解

  1. 头部背景:先通过 RRectLinearGradient 绘制一个带高光渐变的圆角矩形,模拟金属质感。
  2. 天线 & 凹槽:用两个小圆角矩形画出天线,再在顶部中心绘制一个细长凹槽,增加科幻感。
  3. 面板底色:在头部内部绘制深色矩形,作为机器人“面板”的底色。
  4. 眼睛:在两侧绘制可缩放的圆角矩形,并叠加发光和细线条,让眨眼效果更柔和且富有层次感。
  5. 嘴巴:通过缩放 Rect 实现张合动作,并配合朦胧发光,营造出“说话中”的动态效果。

4. 效果演示及扩展建议 💡

最终效果如图所示,小 AI 机器人会左右摇头、眨眼、张嘴,非常生动可爱!

  • 扩展思路:可以接入语音识别,让机器人根据声音节奏摇头和说话~
  • 性能优化:对于复杂渐变和模糊效果,可以考虑提前缓存为图像,减少每帧绘制成本。

结语 🙏

以上就是本次 Flutter AI Bot 动画的完整源码解析与思路拆解,希望能给大家带来灵感🔥。如果你有任何问题或更酷的玩法,欢迎在评论区交流~