Skip to content

动画与自定义绘制 ​

先选成本合适的方案 ​

场景方案需要维护
样式随状态平滑变化AnimatedContainer、AnimatedOpacity目标值、时长与曲线
内容切换AnimatedSwitcher子组件身份与过渡
页面间共享元素Hero唯一 tag 与两侧布局
播放、反向、循环、多个阶段AnimationController + 过渡组件Ticker、控制器和释放
图表、轨迹、进度弧等绘制CustomPaint + CustomPainter尺寸、画笔、重绘与语义
已有布局组件难以表达的布局协议自定义 RenderObject约束、布局、命中测试、绘制和语义

优先组合现有 Widget;只有确认缺少布局或绘制能力,再进入更底层。动画概念可对照 官方 API 概览。

显式动画的生命周期 ​

下面是可放入 MaterialApp.home 的独立页面,文件顶部需导入 package:flutter/material.dart。

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

  @override
  State<FadeNotePage> createState() => _FadeNotePageState();
}

class _FadeNotePageState extends State<FadeNotePage>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('笔记动画')),
      body: Center(
        child: FadeTransition(
          opacity: _controller,
          child: const Card(
            child: Padding(
              padding: EdgeInsets.all(24),
              child: Text('从状态到画面'),
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _controller.forward(from: 0),
        child: const Icon(Icons.replay),
      ),
    );
  }
}

AnimationController 产生动画值,Tween 映射值域,Curve 调整变化节奏,过渡组件消费动画。这里只有透明度变化,直接使用控制器的 0~1 值即可。控制器不能在每次 build 时重新创建;多个控制器的场景需要合适的 TickerProvider。

用 Canvas 画一个进度弧 ​

把下面的 Painter 加到同一文件,另增加 import 'dart:math' as math;:

dart
class ProgressPainter extends CustomPainter {
  ProgressPainter({required this.progress, required this.color});

  final double progress;
  final Color color;

  @override
  void paint(Canvas canvas, Size size) {
    final rect = (Offset.zero & size).deflate(6);
    final paint = Paint()
      ..color = color
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8
      ..strokeCap = StrokeCap.round;
    canvas.drawArc(
      rect,
      -math.pi / 2,
      math.pi * 2 * progress.clamp(0.0, 1.0),
      false,
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant ProgressPainter oldDelegate) {
    return oldDelegate.progress != progress || oldDelegate.color != color;
  }
}

在组件树中用 SizedBox(width: 120, height: 120, child: CustomPaint(painter: ProgressPainter(progress: 0.65, color: Colors.teal))) 展示静态进度,宽高应大于描边留白。加入语义说明时,可在外层使用 Semantics(label: '完成进度', value: '65%')。

shouldRepaint 用于比较新旧绘制委托的输入,并不是「返回 false 后永远不绘制」。动画驱动的绘制还可以通过 Painter 的 repaint 参数监听变化,减少不必要的构建和布局。参见 CustomPainter API。

什么时候才需要 RenderObject ​

现有 Widget 的组合可以表达布局时,组合通常更容易维护。确实需要自定义布局协议时,再研究 RenderBox.performLayout,确保最终尺寸满足约束;属性变化后,根据影响调用 markNeedsLayout 或 markNeedsPaint。

不要把绘制、交互与无障碍拆开考虑:Canvas 画出来的按钮不会自动拥有标准按钮的焦点、点击和语义行为。绘制案例进入业务页面前,要补上这些能力。

练习与验收 ​

让进度从 0 动画到 0.65,检查起点、终点和主题切换后的颜色。重复进入和退出页面不应产生 Ticker 泄漏;再用 性能工具 检查重建与重绘范围。

持续记录,持续理解。