第十八讲 渲染原理与自定义绘制

0 阅读6分钟

前言:

可以跳过。

一、定位

本讲是 Flutter 进阶开发的核心内容,主要解决以下问题:

  • 理解 Flutter 界面渲染的底层逻辑(Widget/Element/RenderObject 三棵树),告别"黑盒式"开发

  • 掌握 setState 刷新机制,避免无效重建和性能问题

  • 学会使用 CustomPaint 实现自定义绘制,突破内置组件的视觉限制

  • 掌握常用视觉特效(裁剪、变形、透明度、混合模式)的使用场景

  • 运用 RepaintBoundary 优化重绘性能,提升复杂界面流畅度

渲染三棵树:Widget(配置)→ Element(实例)→ RenderObject(渲染),setState 仅标记 Element 为 dirty 触发更新

自定义绘制:通过 CustomPaint + CustomPainter 实现任意图形,shouldRepaint 是性能优化关键

性能优化:RepaintBoundary 隔离重绘区域,合理拆分组件避免无效重建,视觉特效按需使用

三棵树关系与渲染流程

image.png

树类型核心作用关键特性
Widget界面配置描述轻量、不可变、可复用
Element实例化桥梁持有Widget和RenderObject引用,决定是否重建
RenderObject渲染执行体处理布局(Layout)、绘制(Paint)、HitTest

二、核心知识点

2.1 三棵树与setState刷新机制

核心原理
  • Widget是「配置模板」,Element 是「模板实例」,RenderObject是「渲染工人」
  • setState 本质是标记当前 Element 为 dirty,Flutter 引擎在下一帧会重新构建该 Element 对应的 Widget,并更新 RenderObject
  • 重建范围:默认会从调用 setState 的 Widget 开始,递归重建所有子 Widget
案例:setState 重建范围演示
import 'package:flutter/material.dart';

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

  @override
  State<ThreeTreesDemo> createState() => _ThreeTreesDemoState();
}

class _ThreeTreesDemoState extends State<ThreeTreesDemo> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    print("外层Widget重建"); // 每次setState都会打印
    return Scaffold(
      appBar: AppBar(title: const Text("三棵树与setState")),
      body: Column(
        children: [
          Text("计数:$_count"),
          // 用RepaintBoundary隔离+避免重建
          RepaintBoundary(
            child: _StaticWidget(), // 静态组件不会被重建
          ),
          ElevatedButton(
            onPressed: () => setState(() => _count++),
            child: const Text("点击增加"),
          )
        ],
      ),
    );
  }
}

// 抽离静态组件,避免被父级setState重建
class _StaticWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("静态Widget重建"); // 只会打印一次
    return Container(
      width: 200,
      height: 200,
      color: Colors.blue,
      child: const Center(child: Text("静态内容")),
    );
  }
}

注意事项
  1. 避免在 build 方法中创建新对象(如 TextStyle、List),否则会触发不必要的重建
  2. 静态组件要抽离为独立 Widget,减少重建范围
  3. setState 是「标记更新」而非「立即刷新」,会在下一帧批量处理

2.2 CustomPaint 自定义绘制

核心属性
属性作用
painter自定义绘制逻辑(必须,继承 CustomPainter)
foregroundPainter前景绘制(覆盖子组件)
size绘制区域大小(默认子组件大小)
willChange标记是否频繁变化,优化性能
案例:绘制自定义圆形进度条
import 'package:flutter/material.dart';
import 'dart:ui' as ui;

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

  @override
  State<CustomPaintDemo> createState() => _CustomPaintDemoState();
}

class _CustomPaintDemoState extends State<CustomPaintDemo> {
  double _progress = 0.5; // 50%进度

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("CustomPaint 自定义绘制")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 自定义绘制组件
            CustomPaint(
              size: const Size(200, 200),
              painter: ProgressPainter(progress: _progress),
              child: Center(
                child: Text(
                  "${(_progress * 100).toInt()}%",
                  style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                ),
              ),
            ),
            // 进度调节
            Slider(
              value: _progress,
              onChanged: (v) => setState(() => _progress = v),
            )
          ],
        ),
      ),
    );
  }
}

// 自定义绘制器
class ProgressPainter extends CustomPainter {
  final double progress;

  ProgressPainter({required this.progress});

  @override
  void paint(Canvas canvas, Size size) {
    // 1. 配置画笔
    final Paint bgPaint = Paint()
      ..color = Colors.grey[300]!
      ..style = PaintingStyle.stroke
      ..strokeWidth = 10;

    final Paint progressPaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 10
      ..strokeCap = StrokeCap.round; // 圆角端点

    // 2. 绘制背景圆
    Offset center = Offset(size.width / 2, size.height / 2);
    double radius = size.width / 2 - 5;
    canvas.drawCircle(center, radius, bgPaint);

    // 3. 绘制进度弧
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      -ui.pi / 2, // 起始角度(12点钟方向)
      2 * ui.pi * progress, // 扫过角度
      false, // 是否闭合
      progressPaint,
    );
  }

  // 关键:判断是否需要重绘(优化性能)
  @override
  bool shouldRepaint(covariant ProgressPainter oldDelegate) {
    return oldDelegate.progress != progress; // 只有进度变化时才重绘
  }
}

注意事项
  1. shouldRepaint 必须实现,返回 false 可避免无效重绘
  2. 绘制复杂图形时,尽量使用 Path 而非多次绘制基础图形
  3. 避免在 paint 方法中创建新对象(如 Paint),可提前初始化

2.3 常用视觉特效

核心属性与案例
特效核心属性案例代码
Clip(裁剪)ClipRect/ClipRRect/ClipPathClipRRect(borderRadius: BorderRadius.circular(20), child: Image.asset("img.png"))
Transform(变形)transform(Matrix4)、alignmentTransform.rotate(angle: pi/4, child: Container(width: 100, height: 100, color: Colors.red))
Opacity(透明度)opacity(0-1)Opacity(opacity: 0.5, child: Text("半透明文字"))
BlendMode(混合模式)blendMode(如BlendMode.srcOver)ColorFiltered(colorFilter: ColorFilter.mode(Colors.red, BlendMode.overlay), child: Image.asset("img.png"))
综合案例:特效组合使用
import 'package:flutter/material.dart';
import 'dart:math' as math;

class EffectsDemo extends StatelessWidget {
  const EffectsDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("视觉特效组合")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 裁剪 + 变形 + 透明度 + 混合模式
            Opacity(
              opacity: 0.8,
              child: Transform(
                transform: Matrix4.rotationZ(math.pi / 12) // 旋转15度
                  ..scale(0.9), // 缩放0.9倍
                alignment: Alignment.center,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(30),
                  child: ColorFiltered(
                    colorFilter: ColorFilter.mode(
                      Colors.pink.withOpacity(0.5),
                      BlendMode.softLight, // 柔光混合
                    ),
                    child: Container(
                      width: 200,
                      height: 200,
                      color: Colors.blue,
                      child: const Center(
                        child: Text(
                          "特效组合",
                          style: TextStyle(fontSize: 24, color: Colors.white),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

注意事项
  1. Transform 是「视觉变形」,不影响布局大小,可能导致点击区域偏移
  2. Opacity 值为0时,组件仍会参与布局,可结合 Offstage 隐藏
  3. BlendMode 会增加绘制开销,复杂界面需配合 RepaintBoundary 使用

2.4 RepaintBoundary 重绘隔离

核心作用
  • 将组件包裹在 RepaintBoundary 中,可使该组件的重绘独立于父组件
  • 避免一个小组件变化导致整个页面重绘,提升性能
  • 可通过 Flutter DevTools 的「Paint Profiler」查看重绘区域
案例:重绘隔离优化
import 'package:flutter/material.dart';

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

  @override
  State<RepaintBoundaryDemo> createState() => _RepaintBoundaryDemoState();
}

class _RepaintBoundaryDemoState extends State<RepaintBoundaryDemo> 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 Scaffold(
      appBar: AppBar(title: const Text("RepaintBoundary 重绘隔离")),
      body: Column(
        children: [
          // 无隔离:动画会导致整个Column重绘
          const Text("无RepaintBoundary(整个区域重绘)"),
          AnimatedBuilder(
            animation: _controller,
            builder: (context, child) {
              return Container(
                width: 100 + _controller.value * 100,
                height: 100,
                color: Colors.red,
              );
            },
          ),
          const SizedBox(height: 20),
          // 有隔离:仅动画区域重绘
          const Text("有RepaintBoundary(仅动画区域重绘)"),
          RepaintBoundary( // 关键:重绘隔离
            child: AnimatedBuilder(
              animation: _controller,
              builder: (context, child) {
                return Container(
                  width: 100 + _controller.value * 100,
                  height: 100,
                  color: Colors.green,
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

注意事项
  1. 不要滥用 RepaintBoundary,每个隔离区域会增加内存开销
  2. 动画、频繁刷新的组件优先使用 RepaintBoundary
  3. 可通过 Flutter DevTools 的「Performance」面板查看重绘情况

三、全章节技术综合应用案例:自定义动态仪表盘

功能说明

整合「三棵树原理+setState优化+CustomPaint绘制+视觉特效+RepaintBoundary」,实现一个带动态效果的仪表盘:

  • 自定义绘制仪表盘刻度、指针
  • 指针随滑块动态旋转(Transform)
  • 刻度值半透明显示(Opacity)
  • 仪表盘圆角裁剪(ClipRRect)
  • 重绘隔离优化性能(RepaintBoundary)
  • 避免无效重建(优化setState范围)

完整代码

import 'package:flutter/material.dart';
import 'dart:ui' as ui;
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: 'Flutter 渲染原理综合案例',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const DashboardDemo(),
    );
  }
}

// 仪表盘主页面(优化setState范围)
class DashboardDemo extends StatefulWidget {
  const DashboardDemo({super.key});

  @override
  State<DashboardDemo> createState() => _DashboardDemoState();
}

class _DashboardDemoState extends State<DashboardDemo> {
  double _value = 50; // 0-100的仪表盘数值

  @override
  Widget build(BuildContext context) {
    // 仅外层build,静态内容不重复构建
    return Scaffold(
      appBar: AppBar(title: const Text("自定义动态仪表盘")),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 重绘隔离:仅仪表盘区域重绘
          RepaintBoundary(
            child: _DashboardView(value: _value),
          ),
          const SizedBox(height: 40),
          // 数值调节滑块
          SizedBox(
            width: 300,
            child: Slider(
              min: 0,
              max: 100,
              value: _value,
              onChanged: (v) => setState(() => _value = v),
              activeColor: Colors.orange,
            ),
          ),
          Text(
            "当前值:${_value.toStringAsFixed(0)}",
            style: const TextStyle(fontSize: 18),
          )
        ],
      ),
    );
  }
}

// 仪表盘视图(抽离为独立组件,减少重建)
class _DashboardView extends StatelessWidget {
  final double value;

  const _DashboardView({required this.value});

  @override
  Widget build(BuildContext context) {
    // 计算指针旋转角度(0-100对应-120°到120°)
    double angle = (value / 100) * 240 - 120;
    angle = angle * math.pi / 180; // 转弧度

    return ClipRRect(
      // 圆角裁剪
      borderRadius: BorderRadius.circular(20),
      child: Container(
        width: 300,
        height: 300,
        color: Colors.grey[100],
        child: Stack(
          alignment: Alignment.center,
          children: [
            // 1. 自定义绘制仪表盘刻度
            CustomPaint(
              size: const Size(300, 300),
              painter: DashboardPainter(),
            ),
            // 2. 指针(变形+透明度)
            Opacity(
              opacity: 0.9,
              child: Transform.rotate(
                angle: angle,
                child: Container(
                  width: 120,
                  height: 8,
                  decoration: BoxDecoration(
                    color: Colors.red,
                    borderRadius: BorderRadius.circular(4),
                    boxShadow: const [
                      BoxShadow(color: Colors.redAccent, blurRadius: 5)
                    ],
                  ),
                ),
              ),
            ),
            // 3. 中心圆点(混合模式)
            ColorFiltered(
              colorFilter: ColorFilter.mode(
                Colors.orange,
                BlendMode.overlay,
              ),
              child: Container(
                width: 20,
                height: 20,
                decoration: const BoxDecoration(
                  color: Colors.white,
                  shape: BoxShape.circle,
                  boxShadow: [BoxShadow(blurRadius: 3)],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// 仪表盘刻度绘制器
class DashboardPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // 移动画布原点到中心
    canvas.translate(size.width / 2, size.height / 2);
    // 反向Y轴(Flutter默认Y轴向下,调整为向上)
    canvas.scale(1, -1);

    // 1. 绘制外圆
    final outerPaint = Paint()
      ..color = Colors.blue[200]!
      ..style = PaintingStyle.stroke
      ..strokeWidth = 4;
    canvas.drawCircle(Offset.zero, size.width / 2 - 20, outerPaint);

    // 2. 绘制刻度(240°范围,每10°一个刻度)
    final gradPaint = Paint()..color = Colors.blue;
    for (int i = 0; i <= 24; i++) {
      double angle = (-120 + i * 10) * math.pi / 180;
      double r1 = size.width / 2 - 20;
      double r2 = r1 - (i % 6 == 0 ? 20 : 10); // 每6个刻度长一点

      // 绘制刻度线
      canvas.drawLine(
        Offset(r1 * math.cos(angle), r1 * math.sin(angle)),
        Offset(r2 * math.cos(angle), r2 * math.sin(angle)),
        gradPaint..strokeWidth = i % 6 == 0 ? 3 : 1,
      );

      // 绘制刻度值(每60°一个)
      if (i % 6 == 0) {
        double textAngle = angle;
        double textR = r1 - 30;
        // 恢复Y轴方向绘制文字
        canvas.save();
        canvas.scale(1, -1);
        TextPainter(
          text: TextSpan(
            text: "${i * 100 / 24}",
            style: const TextStyle(color: Colors.black, fontSize: 14),
          ),
          textDirection: TextDirection.ltr,
        )
          ..layout()
          ..paint(
            canvas,
            Offset(
              textR * math.cos(textAngle) - 10,
              textR * math.sin(textAngle) - 8,
            ),
          );
        canvas.restore();
      }
    }
  }

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

效果说明

  1. 滑块拖动时,仪表盘指针实时旋转,数值同步更新
  2. 自定义绘制的刻度清晰,刻度值自动计算
  3. 通过 RepaintBoundary 隔离,仅仪表盘区域重绘,性能最优
  4. 结合了裁剪、变形、透明度、混合模式等所有视觉特效
  5. 优化了 setState 重建范围,仅必要部分刷新

开发建议

  1. 复杂界面先分析渲染流程,再动手编码
  2. 自定义绘制优先复用 Paint/Path 对象,减少内存开销
  3. 所有动画/频繁刷新组件都应考虑 RepaintBoundary 隔离
  4. 利用 Flutter DevTools 分析重绘和重建,定位性能瓶颈