← 返回学习路线
🔥

进阶技能

深入掌握 Flutter 动画系统、平台通信、自定义绘制、性能优化与国际化等高级技术

🎬 第一章:隐式动画

隐式动画是 Flutter 中最简单的动画方式。你只需要声明目标值,Flutter 会自动在旧值和新值之间进行平滑过渡。所有隐式动画 Widget 都以 Animated 开头。

1.1 AnimatedContainer

AnimatedContainer 可以自动对容器的大小、颜色、边框、圆角等属性变化进行动画。它是最常用的隐式动画组件。

Dart
class AnimatedBoxDemo extends StatefulWidget {
  @override
  State<AnimatedBoxDemo> createState() => _AnimatedBoxDemoState();
}

class _AnimatedBoxDemoState extends State<AnimatedBoxDemo> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _expanded = !_expanded),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 400),
        curve: Curves.easeInOut,
        width: _expanded ? 250 : 100,
        height: _expanded ? 250 : 100,
        decoration: BoxDecoration(
          color: _expanded ? Colors.blue : Colors.red,
          borderRadius: BorderRadius.circular(
            _expanded ? 32 : 8,
          ),
          boxShadow: [
            BoxShadow(
              color: Colors.black26,
              blurRadius: _expanded ? 20 : 5,
            ),
          ],
        ),
        child: const Center(
          child: Text('点击切换', style: TextStyle(color: Colors.white)),
        ),
      ),
    );
  }
}

1.2 AnimatedOpacity

AnimatedOpacity 用于实现淡入淡出效果,常用于显示/隐藏元素的场景。

Dart
AnimatedOpacity(
  opacity: _visible ? 1.0 : 0.0,
  duration: const Duration(milliseconds: 500),
  curve: Curves.easeIn,
  child: Container(
    width: 200,
    height: 200,
    color: Colors.green,
    child: const Center(child: Text('渐隐渐现')),
  ),
)

1.3 AnimatedPositioned

AnimatedPositioned 必须在 Stack 中使用,可以对子组件的位置进行平滑动画。

Dart
Stack(
  children: [
    AnimatedPositioned(
      duration: const Duration(milliseconds: 600),
      curve: Curves.elasticOut,
      left: _moved ? 200 : 20,
      top: _moved ? 150 : 20,
      child: Container(
        width: 80,
        height: 80,
        decoration: BoxDecoration(
          color: Colors.orange,
          borderRadius: BorderRadius.circular(12),
        ),
      ),
    ),
  ],
)

1.4 AnimatedSwitcher

AnimatedSwitcher 在子组件切换时自动播放过渡动画。需要给子组件不同的 key 才能触发动画。

Dart
AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  transitionBuilder: (Widget child, Animation<double> animation) {
    return ScaleTransition(scale: animation, child: child);
  },
  child: Text(
    '$_count',
    key: ValueKey<int>(_count),
    style: const TextStyle(fontSize: 48),
  ),
)

1.5 TweenAnimationBuilder

TweenAnimationBuilder 允许你使用任意 Tween 创建隐式动画,比内置的隐式动画组件更灵活。

Dart
TweenAnimationBuilder<double>(
  tween: Tween<double>(begin: 0, end: _angle),
  duration: const Duration(milliseconds: 800),
  curve: Curves.easeOutBack,
  builder: (context, value, child) {
    return Transform.rotate(
      angle: value,
      child: child,
    );
  },
  child: const Icon(Icons.refresh, size: 60),
)
💡 提示:所有隐式动画组件都支持 durationcurve 参数。curve 控制动画的缓动效果,常用的有 Curves.easeInOutCurves.bounceOutCurves.elasticOut 等。

✨ 常用隐式动画组件

  • AnimatedContainer - 容器属性动画
  • AnimatedOpacity - 透明度动画
  • AnimatedPadding - 内边距动画
  • AnimatedAlign - 对齐动画
  • AnimatedDefaultTextStyle - 文本样式动画
  • AnimatedCrossFade - 交叉淡入淡出

🎯 使用场景

  • 按钮点击后的视觉反馈
  • 列表项展开/收起
  • 页面元素的渐入效果
  • 主题切换过渡
  • 状态指示器变化

🎭 第二章:显式动画

显式动画提供了对动画过程的完全控制。你需要手动管理 AnimationController,但可以实现更复杂的动画效果,如循环、反向、组合动画等。

2.1 AnimationController

AnimationController 是显式动画的核心,它控制动画的播放、暂停、反向等。需要配合 SingleTickerProviderStateMixin 使用。

Dart
class PulseAnimation extends StatefulWidget {
  @override
  State<PulseAnimation> createState() => _PulseAnimationState();
}

class _PulseAnimationState extends State<PulseAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )..repeat(reverse: true); // 循环播放并反向
  }

  @override
  void dispose() {
    _controller.dispose(); // 必须释放资源
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: _controller,
      child: const FlutterLogo(size: 100),
    );
  }
}

2.2 Tween 与 CurvedAnimation

Tween 定义动画的值范围,CurvedAnimation 为动画添加缓动曲线。两者组合可以精确控制动画的表现。

Dart
late AnimationController _controller;
late Animation<double> _sizeAnimation;
late Animation<Color?> _colorAnimation;

@override
void initState() {
  super.initState();
  _controller = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 1500),
  );

  // 带缓动曲线的动画
  final curvedAnimation = CurvedAnimation(
    parent: _controller,
    curve: Curves.easeOutBack,
  );

  // 大小动画:50 -> 200
  _sizeAnimation = Tween<double>(
    begin: 50,
    end: 200,
  ).animate(curvedAnimation);

  // 颜色动画:红 -> 蓝
  _colorAnimation = ColorTween(
    begin: Colors.red,
    end: Colors.blue,
  ).animate(curvedAnimation);
}

2.3 AnimatedBuilder

AnimatedBuilder 用于将动画与 Widget 解耦,减少不必要的重建,是性能最佳的显式动画方式。

Dart
@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _controller,
    builder: (context, child) {
      return Container(
        width: _sizeAnimation.value,
        height: _sizeAnimation.value,
        decoration: BoxDecoration(
          color: _colorAnimation.value,
          borderRadius: BorderRadius.circular(16),
        ),
        child: child, // child 不会重建
      );
    },
    child: const Center(
      child: Icon(Icons.star, color: Colors.white, size: 30),
    ),
  );
}

2.4 交错动画 (Staggered Animations)

交错动画通过 Interval 让多个动画按顺序执行,创造丰富的视觉效果。

Dart
class StaggeredDemo extends StatefulWidget {
  @override
  State<StaggeredDemo> createState() => _StaggeredDemoState();
}

class _StaggeredDemoState extends State<StaggeredDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _opacity;
  late Animation<double> _width;
  late Animation<double> _height;
  late Animation<EdgeInsets> _padding;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 2000),
    );

    // 0% - 30%:淡入
    _opacity = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.0, 0.3, curve: Curves.easeIn),
      ),
    );

    // 20% - 60%:宽度展开
    _width = Tween<double>(begin: 50, end: 250).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.2, 0.6, curve: Curves.easeOut),
      ),
    );

    // 40% - 80%:高度展开
    _height = Tween<double>(begin: 50, end: 250).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.4, 0.8, curve: Curves.easeOut),
      ),
    );

    // 60% - 100%:内边距变化
    _padding = EdgeInsetsTween(
      begin: const EdgeInsets.all(0),
      end: const EdgeInsets.all(20),
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.6, 1.0, curve: Curves.easeInOut),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Opacity(
          opacity: _opacity.value,
          child: Container(
            width: _width.value,
            height: _height.value,
            padding: _padding.value,
            decoration: BoxDecoration(
              color: Colors.deepPurple,
              borderRadius: BorderRadius.circular(16),
            ),
            child: const FlutterLogo(),
          ),
        );
      },
    );
  }
}

2.5 完整示例:旋转缩放动画

Dart
class RotateScaleDemo extends StatefulWidget {
  @override
  State<RotateScaleDemo> createState() => _RotateScaleDemoState();
}

class _RotateScaleDemoState extends State<RotateScaleDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _rotation;
  late Animation<double> _scale;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 3),
    )..repeat();

    _rotation = Tween<double>(begin: 0, end: 2 * 3.14159)
        .animate(_controller);

    _scale = TweenSequence<double>([
      TweenSequenceItem(
        tween: Tween(begin: 1.0, end: 1.5),
        weight: 50,
      ),
      TweenSequenceItem(
        tween: Tween(begin: 1.5, end: 1.0),
        weight: 50,
      ),
    ]).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.rotate(
          angle: _rotation.value,
          child: Transform.scale(
            scale: _scale.value,
            child: child,
          ),
        );
      },
      child: Container(
        width: 80,
        height: 80,
        decoration: BoxDecoration(
          gradient: const LinearGradient(
            colors: [Colors.purple, Colors.blue],
          ),
          borderRadius: BorderRadius.circular(16),
        ),
        child: const Icon(Icons.star, color: Colors.white, size: 40),
      ),
    );
  }
}
⚠️ 注意:使用 AnimationController 时,必须dispose() 中调用 _controller.dispose() 释放资源,否则会导致内存泄漏。如果有多个 AnimationController,使用 TickerProviderStateMixin 代替 SingleTickerProviderStateMixin
对比项隐式动画显式动画
复杂度简单,声明式较复杂,需要手动管理
控制力仅目标值变化完全控制(播放/暂停/反向/循环)
性能一般可通过 AnimatedBuilder 优化
适用场景简单状态切换复杂、连续、组合动画
代码量

🦸 第三章:Hero 动画

Hero 动画用于在页面导航时创建共享元素过渡效果。典型场景是列表页的图片点击后「飞入」详情页。两个页面中的 Hero Widget 需要使用相同的 tag

3.1 基本用法

Dart
// ===== 列表页 =====
class ProductListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('商品列表')),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          final product = products[index];
          return GestureDetector(
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(
                builder: (_) => ProductDetailPage(product: product),
              ),
            ),
            child: Hero(
              tag: 'product-${product.id}',
              child: Image.network(product.imageUrl,
                width: 80, height: 80, fit: BoxFit.cover),
            ),
          );
        },
      ),
    );
  }
}

// ===== 详情页 =====
class ProductDetailPage extends StatelessWidget {
  final Product product;
  const ProductDetailPage({required this.product});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(product.name)),
      body: Column(
        children: [
          Hero(
            tag: 'product-${product.id}',
            child: Image.network(product.imageUrl,
              width: double.infinity, height: 300, fit: BoxFit.cover),
          ),
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text(product.description),
          ),
        ],
      ),
    );
  }
}

3.2 自定义飞行动画 (flightShuttleBuilder)

使用 flightShuttleBuilder 可以自定义动画过渡时元素的外观,比如给飞行中的元素添加阴影、变形等效果。

Dart
Hero(
  tag: 'avatar-hero',
  flightShuttleBuilder: (
    BuildContext flightContext,
    Animation<double> animation,
    HeroFlightDirection flightDirection,
    BuildContext fromHeroContext,
    BuildContext toHeroContext,
  ) {
    return AnimatedBuilder(
      animation: animation,
      builder: (context, child) {
        return Material(
          color: Colors.transparent,
          child: Container(
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              boxShadow: [
                BoxShadow(
                  color: Colors.black26,
                  blurRadius: 20 * animation.value,
                  spreadRadius: 4 * animation.value,
                ),
              ],
            ),
            child: ClipOval(
              child: Image.network('avatar_url', fit: BoxFit.cover),
            ),
          ),
        );
      },
    );
  },
  child: ClipOval(
    child: Image.network('avatar_url',
      width: 50, height: 50, fit: BoxFit.cover),
  ),
)

3.3 Hero 动画与自定义路由过渡

Dart
// 自定义页面路由,让 Hero 动画更丝滑
Navigator.push(
  context,
  PageRouteBuilder(
    transitionDuration: const Duration(milliseconds: 600),
    reverseTransitionDuration: const Duration(milliseconds: 400),
    pageBuilder: (context, animation, secondaryAnimation) {
      return FadeTransition(
        opacity: animation,
        child: ProductDetailPage(product: product),
      );
    },
  ),
);
📝 备注:Hero 动画的 tag 必须在同一页面内唯一。如果列表中有多个 Hero,每个都应该有不同的 tag(如使用 id 区分)。Hero 动画在 Navigator.pushNavigator.pop 时都会触发。

📱 第四章:Platform Channel

Platform Channel 是 Flutter 与原生平台(Android/iOS)之间的通信桥梁。通过它可以调用原生 API、获取设备信息、使用原生 SDK 等。Flutter 提供了三种 Channel 类型。

4.1 MethodChannel

MethodChannel 用于一次性的方法调用,支持请求-响应模式。是最常用的通信方式。

Flutter 端

Dart
import 'package:flutter/services.dart';

class BatteryService {
  static const _channel = MethodChannel('com.example.app/battery');

  // 获取电池电量
  static Future<int> getBatteryLevel() async {
    try {
      final int level = await _channel.invokeMethod('getBatteryLevel');
      return level;
    } on PlatformException catch (e) {
      throw Exception('获取电池电量失败: ${e.message}');
    }
  }

  // 带参数的调用
  static Future<String> getDeviceInfo(String key) async {
    final result = await _channel.invokeMethod('getDeviceInfo', {
      'key': key,
    });
    return result as String;
  }
}

// 在 Widget 中使用
ElevatedButton(
  onPressed: () async {
    final level = await BatteryService.getBatteryLevel();
    setState(() => _batteryLevel = level);
  },
  child: const Text('获取电池电量'),
)

Android 端 (Kotlin)

Kotlin
class MainActivity: FlutterActivity() {
    private val CHANNEL = "com.example.app/battery"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "getBatteryLevel" -> {
                        val batteryManager =
                            getSystemService(Context.BATTERY_SERVICE) as BatteryManager
                        val level = batteryManager.getIntProperty(
                            BatteryManager.BATTERY_PROPERTY_CAPACITY
                        )
                        if (level != -1) {
                            result.success(level)
                        } else {
                            result.error("UNAVAILABLE", "无法获取电池电量", null)
                        }
                    }
                    else -> result.notImplemented()
                }
            }
    }
}

iOS 端 (Swift)

Swift
@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions:
            [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        let controller = window?.rootViewController as! FlutterViewController
        let channel = FlutterMethodChannel(
            name: "com.example.app/battery",
            binaryMessenger: controller.binaryMessenger
        )

        channel.setMethodCallHandler { (call, result) in
            if call.method == "getBatteryLevel" {
                let device = UIDevice.current
                device.isBatteryMonitoringEnabled = true
                let level = Int(device.batteryLevel * 100)
                result(level)
            } else {
                result(FlutterMethodNotImplemented)
            }
        }

        return super.application(application,
            didFinishLaunchingWithOptions: launchOptions)
    }
}

4.2 EventChannel

EventChannel 用于从原生平台向 Flutter 推送持续的事件流,适用于传感器数据、电池状态变化等场景。

Dart
class BatteryEventService {
  static const _eventChannel =
      EventChannel('com.example.app/battery_events');

  // 监听电池状态变化
  static Stream<int> batteryLevelStream() {
    return _eventChannel
        .receiveBroadcastStream()
        .map((event) => event as int);
  }
}

// 在 Widget 中使用
StreamBuilder<int>(
  stream: BatteryEventService.batteryLevelStream(),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text('电池电量: ${snapshot.data}%');
    }
    return const CircularProgressIndicator();
  },
)

4.3 Pigeon(类型安全通信)

Pigeon 是 Flutter 团队推荐的类型安全 Platform Channel 代码生成工具,避免手动编写字符串匹配。

Dart
import 'package:pigeon/pigeon.dart';

// 定义接口(Pigeon 会自动生成各平台代码)
@HostApi()
abstract class BatteryApi {
  int getBatteryLevel();
  String getBatteryState();
}

@FlutterApi()
abstract class BatteryEventApi {
  void onBatteryLevelChanged(int level);
}
💡 提示:Channel 名称必须在 Flutter 端和原生端完全一致。推荐使用反向域名格式(如 com.example.app/feature)来避免命名冲突。对于简单的平台调用,也可以考虑使用社区提供的插件(如 battery_plus),避免手动编写原生代码。
Channel 类型方向模式适用场景
MethodChannel双向请求-响应方法调用、获取数据
EventChannel原生 → Flutter事件流传感器、状态变化
BasicMessageChannel双向消息传递自定义编解码
Pigeon双向类型安全代码生成大型项目推荐

🎨 第五章:自定义绘制

Flutter 提供了 CustomPaintCanvas API,让你可以直接在画布上绘制任意图形。这是实现自定义图表、特殊形状、手写板等功能的基础。

5.1 CustomPaint 基础

CustomPaint 接收一个 CustomPainter 来执行绘制逻辑。

Dart
class CirclePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill;

    // 绘制填充圆
    canvas.drawCircle(
      Offset(size.width / 2, size.height / 2),
      50,
      paint,
    );

    // 绘制边框圆
    paint
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3;
    canvas.drawCircle(
      Offset(size.width / 2, size.height / 2),
      70,
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

// 使用
CustomPaint(
  size: const Size(200, 200),
  painter: CirclePainter(),
)

5.2 Canvas API 常用方法

Dart
void paint(Canvas canvas, Size size) {
  final paint = Paint()
    ..color = Colors.black87
    ..strokeWidth = 2;

  // 绘制线段
  canvas.drawLine(
    const Offset(0, 0),
    Offset(size.width, size.height),
    paint,
  );

  // 绘制矩形
  canvas.drawRect(
    Rect.fromLTWH(20, 20, 100, 60),
    paint..style = PaintingStyle.stroke,
  );

  // 绘制圆角矩形
  canvas.drawRRect(
    RRect.fromRectAndRadius(
      Rect.fromLTWH(20, 100, 100, 60),
      const Radius.circular(12),
    ),
    paint..color = Colors.green,
  );

  // 绘制弧形
  canvas.drawArc(
    Rect.fromCenter(
      center: Offset(size.width / 2, 200),
      width: 120, height: 120,
    ),
    0,          // 起始角度
    3.14,      // 扫描角度(弧度)
    false,     // 是否连接中心点
    paint..color = Colors.purple,
  );

  // 绘制文字
  final textPainter = TextPainter(
    text: const TextSpan(
      text: 'Canvas 文字',
      style: TextStyle(color: Colors.black, fontSize: 16),
    ),
    textDirection: TextDirection.ltr,
  );
  textPainter.layout();
  textPainter.paint(canvas, const Offset(20, 280));
}

5.3 Path 绘制

Path 可以绘制复杂的形状,如波浪线、多边形、贝塞尔曲线等。

Dart
class WavePainter extends CustomPainter {
  final double animationValue;
  WavePainter(this.animationValue);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue.withOpacity(0.6)
      ..style = PaintingStyle.fill;

    final path = Path();
    path.moveTo(0, size.height * 0.5);

    // 绘制波浪
    for (double i = 0; i < size.width; i++) {
      path.lineTo(
        i,
        size.height * 0.5 +
            sin((i / size.width * 2 * pi) +
                (animationValue * 2 * pi)) * 30,
      );
    }

    path.lineTo(size.width, size.height);
    path.lineTo(0, size.height);
    path.close();

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant WavePainter oldDelegate) =>
      oldDelegate.animationValue != animationValue;
}

5.4 Paint 属性详解

Dart
final paint = Paint()
  ..color = Colors.blue                   // 颜色
  ..style = PaintingStyle.stroke          // fill 或 stroke
  ..strokeWidth = 3.0                     // 线宽
  ..strokeCap = StrokeCap.round           // 线段端点样式
  ..strokeJoin = StrokeJoin.round         // 线段连接样式
  ..isAntiAlias = true                    // 抗锯齿
  ..shader = LinearGradient(              // 渐变着色器
      colors: [Colors.blue, Colors.purple],
    ).createShader(Rect.fromLTWH(0, 0, 200, 200))
  ..maskFilter = const MaskFilter.blur(  // 模糊效果
      BlurStyle.normal, 5.0,
    );

5.5 CustomClipper

CustomClipper 用于裁剪 Widget 的形状,常用于创建波浪形、弧形的头部区域。

Dart
class WaveClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final path = Path();
    path.lineTo(0, size.height - 40);

    // 贝塞尔曲线形成波浪
    path.quadraticBezierTo(
      size.width / 4, size.height,
      size.width / 2, size.height - 40,
    );
    path.quadraticBezierTo(
      size.width * 3 / 4, size.height - 80,
      size.width, size.height - 40,
    );

    path.lineTo(size.width, 0);
    path.close();
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}

// 使用
ClipPath(
  clipper: WaveClipper(),
  child: Container(
    height: 200,
    color: Colors.blue,
    child: const Center(
      child: Text('波浪裁剪',
        style: TextStyle(color: Colors.white, fontSize: 24)),
    ),
  ),
)

5.6 完整折线图示例

Dart
class LineChartPainter extends CustomPainter {
  final List<double> data;
  final Color lineColor;
  final Color fillColor;

  LineChartPainter({
    required this.data,
    this.lineColor = Colors.blue,
    this.fillColor = Colors.blue,
  });

  @override
  void paint(Canvas canvas, Size size) {
    if (data.isEmpty) return;

    final maxVal = data.reduce((a, b) => a > b ? a : b);
    final minVal = data.reduce((a, b) => a < b ? a : b);
    final range = maxVal - minVal;
    final stepX = size.width / (data.length - 1);
    const padding = 20.0;

    // 绘制网格线
    final gridPaint = Paint()
      ..color = Colors.grey.withOpacity(0.2)
      ..strokeWidth = 0.5;
    for (int i = 0; i < 5; i++) {
      final y = padding + (size.height - padding * 2) / 4 * i;
      canvas.drawLine(
        Offset(0, y), Offset(size.width, y), gridPaint,
      );
    }

    // 计算数据点
    final points = <Offset>[];
    for (int i = 0; i < data.length; i++) {
      final x = i * stepX;
      final y = size.height - padding -
          ((data[i] - minVal) / range) * (size.height - padding * 2);
      points.add(Offset(x, y));
    }

    // 绘制填充区域
    final fillPath = Path()
      ..moveTo(0, size.height)
      ..lineTo(points.first.dx, points.first.dy);
    for (final p in points) {
      fillPath.lineTo(p.dx, p.dy);
    }
    fillPath
      ..lineTo(size.width, size.height)
      ..close();

    canvas.drawPath(
      fillPath,
      Paint()
        ..shader = LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [fillColor.withOpacity(0.3), fillColor.withOpacity(0.0)],
        ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)),
    );

    // 绘制折线
    final linePaint = Paint()
      ..color = lineColor
      ..strokeWidth = 2.5
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    final linePath = Path()..moveTo(points.first.dx, points.first.dy);
    for (final p in points.skip(1)) {
      linePath.lineTo(p.dx, p.dy);
    }
    canvas.drawPath(linePath, linePaint);

    // 绘制数据点
    final dotPaint = Paint()..color = lineColor;
    for (final p in points) {
      canvas.drawCircle(p, 4, dotPaint);
      canvas.drawCircle(p, 2, Paint()..color = Colors.white);
    }
  }

  @override
  bool shouldRepaint(covariant LineChartPainter oldDelegate) =>
      oldDelegate.data != data;
}

// 使用
CustomPaint(
  size: const Size(double.infinity, 200),
  painter: LineChartPainter(
    data: [30, 45, 28, 62, 55, 78, 40, 90, 65],
    lineColor: Colors.blue,
    fillColor: Colors.blue,
  ),
)
⚠️ 注意:shouldRepaint 返回 true 会导致每帧都重绘。请务必根据实际数据变化来判断是否需要重绘,避免性能浪费。对于复杂绘制,考虑使用 RepaintBoundary 隔离重绘区域。

⚡ 第六章:性能优化

性能优化是进阶开发者的核心技能。Flutter 应用需要保持 60fps(或 120fps)的流畅渲染。以下是关键的优化策略。

6.1 Flutter DevTools

DevTools 是 Flutter 官方提供的性能分析工具,可以分析 Widget 重建、布局耗时、帧率等信息。

Shell
# 在调试模式下启动应用后,打开 DevTools
flutter run
# 控制台会输出 DevTools 链接
# 或者直接通过命令打开
dart devtools

# 使用 Profile 模式运行(更接近真实性能)
flutter run --profile

📊 DevTools 核心功能

  • Widget Inspector - 查看 Widget 树结构
  • Timeline - 分析帧渲染耗时
  • Memory - 内存使用分析
  • Performance Overlay - 实时帧率监控
  • CPU Profiler - 函数调用耗时
  • Network - 网络请求监控

🚨 性能警告信号

  • 帧渲染时间超过 16ms
  • 频繁不必要的 build 调用
  • 内存使用持续增长
  • Shader 编译导致的 Jank
  • 列表滚动卡顿
  • 图片加载导致的掉帧

6.2 使用 const 构造函数

const 修饰的 Widget 在编译期创建,不会在每次 build 时重新实例化,可以大幅减少不必要的重建。

Dart
// 不好 - 每次 build 都会创建新实例
Widget build(BuildContext context) {
  return Padding(
    padding: EdgeInsets.all(8), // 每次创建新对象
    child: Text(
      '静态文本',
      style: TextStyle(fontSize: 16), // 每次创建新对象
    ),
  );
}

// 好 - 使用 const,编译期常量
Widget build(BuildContext context) {
  return const Padding(
    padding: EdgeInsets.all(8),
    child: Text(
      '静态文本',
      style: TextStyle(fontSize: 16),
    ),
  );
}

6.3 RepaintBoundary

RepaintBoundary 将子树隔离到独立的图层,避免父组件变化时重绘子组件。适用于动画和静态区域混合的场景。

Dart
Stack(
  children: [
    // 背景动画不会影响前景列表的重绘
    RepaintBoundary(
      child: AnimatedBackground(), // 频繁重绘的动画
    ),
    RepaintBoundary(
      child: ProductList(), // 不需要频繁重绘
    ),
  ],
)

// 检查重绘情况:开启调试重绘彩虹
import 'package:flutter/rendering.dart';
debugRepaintRainbowEnabled = true;

6.4 ListView.builder 懒加载

对于长列表,始终使用 ListView.builder 而不是直接传入 children 列表。它只构建可见区域的 Widget。

Dart
// 不好 - 一次性构建所有项
ListView(
  children: items.map((item) => ItemWidget(item: item)).toList(),
)

// 好 - 按需构建可见项
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) => ItemWidget(item: items[index]),
)

// 更好 - 已知固定高度时指定 itemExtent
ListView.builder(
  itemCount: items.length,
  itemExtent: 72, // 跳过布局计算,性能更优
  itemBuilder: (context, index) => ItemWidget(item: items[index]),
)

// 高性能 - 使用 ListView.separated 带分割线
ListView.separated(
  itemCount: items.length,
  itemBuilder: (context, index) => ItemWidget(item: items[index]),
  separatorBuilder: (_, __) => const Divider(),
)

6.5 图片优化

Dart
// 1. 指定缓存尺寸,避免加载原始大图到内存
Image.network(
  'https://example.com/large_image.jpg',
  cacheWidth: 300,  // 缓存缩放后的图片
  cacheHeight: 300,
)

// 2. 使用 cached_network_image 进行磁盘缓存
CachedNetworkImage(
  imageUrl: 'https://example.com/image.jpg',
  placeholder: (context, url) => const CircularProgressIndicator(),
  errorWidget: (context, url, error) => const Icon(Icons.error),
  memCacheWidth: 300,
)

// 3. 预缓存图片(在 didChangeDependencies 中调用)
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  precacheImage(
    const AssetImage('assets/hero_bg.jpg'),
    context,
  );
}

6.6 Isolate 计算隔离

耗时的计算任务(如 JSON 解析、图片处理、数据排序)应该放在 Isolate 中执行,避免阻塞 UI 线程导致掉帧。

Dart
import 'dart:isolate';
import 'package:flutter/foundation.dart';

// 方式一:使用 compute(简单任务)
Future<List<Product>> parseProducts(String jsonStr) async {
  return await compute(_parseJson, jsonStr);
}

// 顶层函数(Isolate 要求)
List<Product> _parseJson(String jsonStr) {
  final List data = jsonDecode(jsonStr);
  return data.map((e) => Product.fromJson(e)).toList();
}

// 方式二:使用 Isolate.run(Dart 2.19+,更简洁)
Future<List<Product>> parseProductsV2(String jsonStr) async {
  return await Isolate.run(() {
    final List data = jsonDecode(jsonStr);
    return data.map((e) => Product.fromJson(e)).toList();
  });
}

// 方式三:长期运行的 Isolate(多次通信)
Future<void> longRunningTask() async {
  final receivePort = ReceivePort();
  await Isolate.spawn(_heavyWork, receivePort.sendPort);

  receivePort.listen((message) {
    print('收到结果: $message');
  });
}

void _heavyWork(SendPort sendPort) {
  // 执行耗时计算...
  sendPort.send('计算完成');
}
💡 性能优化检查清单:
  • 尽量使用 const 构造函数
  • 避免在 build() 方法中执行耗时操作
  • 使用 ListView.builder 替代 ListView(children: [...])
  • 对频繁变化的区域使用 RepaintBoundary
  • 图片指定 cacheWidth / cacheHeight
  • 耗时计算使用 compute()Isolate.run()
  • 使用 Profile 模式测试性能(而非 Debug 模式)
  • const 修饰不变的 Widget 子树
📝 备注:Shader 预热可以解决首次渲染时的编译卡顿。在 flutter build 时使用 --bundle-sksl-path 参数将 SkSL 着色器预编译到应用中:flutter build apk --bundle-sksl-path flutter_01.sksl.json

🌍 第七章:国际化

国际化(i18n)让你的应用支持多种语言和地区。Flutter 官方提供了 flutter_localizationsintl 包来实现国际化。

7.1 添加依赖

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.19.0

flutter:
  generate: true

7.2 配置 l10n.yaml

l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_zh.arb
output-localization-file: app_localizations.dart

7.3 ARB 文件

ARB(Application Resource Bundle)文件是 Flutter 官方推荐的翻译文件格式。每种语言一个文件。

lib/l10n/app_zh.arb
{
  "@@locale": "zh",
  "appTitle": "我的应用",
  "@appTitle": {
    "description": "应用标题"
  },
  "welcome": "你好,{name}!",
  "@welcome": {
    "placeholders": {
      "name": {
        "type": "String"
      }
    }
  },
  "itemCount": "{count, plural, =0{没有项目} =1{1 个项目} other{{count} 个项目}}",
  "@itemCount": {
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  }
}
lib/l10n/app_en.arb
{
  "@@locale": "en",
  "appTitle": "My App",
  "welcome": "Hello, {name}!",
  "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}"
}

7.4 配置 MaterialApp

Dart
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '国际化示例',
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: const [
        Locale('zh'),       // 中文
        Locale('en'),       // 英文
        Locale('ja'),       // 日文
      ],
      locale: const Locale('zh'), // 默认语言
      home: HomePage(),
    );
  }
}

7.5 使用翻译

Dart
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(title: Text(l10n.appTitle)),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(l10n.welcome('Flutter')),   // 你好,Flutter!
          Text(l10n.itemCount(0)),          // 没有项目
          Text(l10n.itemCount(1)),          // 1 个项目
          Text(l10n.itemCount(5)),          // 5 个项目
        ],
      ),
    );
  }
}

7.6 easy_localization(第三方方案)

easy_localization 提供了更简洁的 API 和运行时语言切换功能,适合快速开发。

Dart
import 'package:easy_localization/easy_localization.dart';

// 初始化
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await EasyLocalization.ensureInitialized();

  runApp(
    EasyLocalization(
      supportedLocales: const [
        Locale('en'),
        Locale('zh'),
        Locale('ja'),
      ],
      path: 'assets/translations',
      fallbackLocale: const Locale('en'),
      child: MyApp(),
    ),
  );
}

// 配置 MaterialApp
MaterialApp(
  localizationsDelegates: context.localizationDelegates,
  supportedLocales: context.supportedLocales,
  locale: context.locale,
  home: HomePage(),
)

// 在 Widget 中使用
Text('app_title'.tr())                // 简洁的翻译调用
Text('welcome'.tr(args: ['张三']))    // 带参数
Text('items'.plural(5))              // 复数形式

// 运行时切换语言
context.setLocale(const Locale('en'));
// 获取当前语言
print(context.locale); // zh

7.7 JSON 翻译文件示例

assets/translations/zh.json
{
  "app_title": "我的应用",
  "welcome": "你好,{}!",
  "items": {
    "zero": "没有项目",
    "one": "1 个项目",
    "other": "{} 个项目"
  },
  "settings": {
    "title": "设置",
    "language": "语言",
    "theme": "主题",
    "dark_mode": "深色模式"
  }
}
📝 备注:官方方案(flutter_localizations + intl)适合需要编译期类型安全的大型项目,翻译错误在编译时就能发现。easy_localization 更适合快速开发和需要运行时切换语言的场景。选择时还需考虑团队习惯和项目规模。

🏆 第八章:实践练习

通过以下练习巩固本章所学的进阶技能。建议依次完成,每个练习都会综合运用多个知识点。

📝 练习一:动画登录页

创建一个带有丰富动画效果的登录页面:

  1. Logo 图标使用 Hero 动画从启动页飞入
  2. 表单字段使用交错动画依次滑入(从左到右)
  3. 登录按钮使用 AnimatedContainer 实现点击后缩小为圆形加载状态
  4. 登录成功后使用 AnimatedSwitcher 切换为成功图标
  5. 错误提示使用 AnimatedOpacity 渐入渐出

📝 练习二:自定义仪表盘

使用 CustomPaint 绘制一个数据仪表盘:

  1. 绘制一个半圆形仪表盘(带刻度线和数值标签)
  2. 使用 Path 绘制指针,支持动画旋转
  3. 底部绘制一个折线图展示历史数据趋势
  4. 使用 AnimationController 驱动指针和数据更新动画
  5. 添加 RepaintBoundary 优化性能,确保帧率稳定在 60fps

📝 练习三:多语言天气应用

构建一个支持中英文切换的天气应用:

  1. 使用 flutter_localizationseasy_localization 实现中英文支持
  2. 通过 Platform Channel 获取设备当前位置(或模拟数据)
  3. 天气图标使用 CustomPaint 绘制(太阳、云朵、雨滴)
  4. 页面切换使用 Hero 动画实现城市卡片到详情的过渡
  5. 使用 Isolate 在后台解析天气 JSON 数据
  6. 确保列表使用 ListView.builder 并做好性能优化