前言:
可以跳过。
一、定位
本讲是 Flutter 进阶开发的核心内容,主要解决以下问题:
-
理解 Flutter 界面渲染的底层逻辑(Widget/Element/RenderObject 三棵树),告别"黑盒式"开发
-
掌握 setState 刷新机制,避免无效重建和性能问题
-
学会使用 CustomPaint 实现自定义绘制,突破内置组件的视觉限制
-
掌握常用视觉特效(裁剪、变形、透明度、混合模式)的使用场景
-
运用 RepaintBoundary 优化重绘性能,提升复杂界面流畅度
渲染三棵树:Widget(配置)→ Element(实例)→ RenderObject(渲染),setState 仅标记 Element 为 dirty 触发更新
自定义绘制:通过 CustomPaint + CustomPainter 实现任意图形,shouldRepaint 是性能优化关键
性能优化:RepaintBoundary 隔离重绘区域,合理拆分组件避免无效重建,视觉特效按需使用
三棵树关系与渲染流程
| 树类型 | 核心作用 | 关键特性 |
|---|---|---|
| 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("静态内容")),
);
}
}
注意事项
- 避免在 build 方法中创建新对象(如 TextStyle、List),否则会触发不必要的重建
- 静态组件要抽离为独立 Widget,减少重建范围
- 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; // 只有进度变化时才重绘
}
}
注意事项
shouldRepaint必须实现,返回false可避免无效重绘- 绘制复杂图形时,尽量使用
Path而非多次绘制基础图形 - 避免在
paint方法中创建新对象(如 Paint),可提前初始化
2.3 常用视觉特效
核心属性与案例
| 特效 | 核心属性 | 案例代码 |
|---|---|---|
| Clip(裁剪) | ClipRect/ClipRRect/ClipPath | ClipRRect(borderRadius: BorderRadius.circular(20), child: Image.asset("img.png")) |
| Transform(变形) | transform(Matrix4)、alignment | Transform.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),
),
),
),
),
),
),
),
],
),
),
);
}
}
注意事项
- Transform 是「视觉变形」,不影响布局大小,可能导致点击区域偏移
- Opacity 值为0时,组件仍会参与布局,可结合 Offstage 隐藏
- 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,
);
},
),
),
],
),
);
}
}
注意事项
- 不要滥用 RepaintBoundary,每个隔离区域会增加内存开销
- 动画、频繁刷新的组件优先使用 RepaintBoundary
- 可通过 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;
}
效果说明
- 滑块拖动时,仪表盘指针实时旋转,数值同步更新
- 自定义绘制的刻度清晰,刻度值自动计算
- 通过 RepaintBoundary 隔离,仅仪表盘区域重绘,性能最优
- 结合了裁剪、变形、透明度、混合模式等所有视觉特效
- 优化了 setState 重建范围,仅必要部分刷新
开发建议
- 复杂界面先分析渲染流程,再动手编码
- 自定义绘制优先复用 Paint/Path 对象,减少内存开销
- 所有动画/频繁刷新组件都应考虑 RepaintBoundary 隔离
- 利用 Flutter DevTools 分析重绘和重建,定位性能瓶颈