在AI的大趋势之下, 国内一片稳中向好, 但只有我早上送外卖、中午送快递、晚上开滴滴…… 上厕所拉起裤子的间隙会想:妈蛋,上辈子我一定是毁灭宇宙了,这辈子才让我这样….

今天, 学习一下自定义Widget.

Flutter中的自定义Widget ,从简单的通过 CustomPainter绘制, 到复杂的通过自定义 renderObject实现,各自有着各自的使用场景.

CustomPainter

ok,先从CustomPainter源码看下它的基本构成.从他的构造函数可以看出来

点击显示代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
class CustomPaint extends SingleChildRenderObjectWidget {
/// Creates a widget that delegates its painting.
// 创建一个将其绘制工作委托出去的 widget。
const CustomPaint({
super.key,
this.painter,
this.foregroundPainter,
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
super.child,
}) : assert(painter != null || foregroundPainter != null || (!isComplex && !willChange));

/// The painter that paints before the children.
// 在子 widget 之前进行绘制的 painter。
final CustomPainter? painter;

/// The painter that paints after the children.
// 在子 widget 之后进行绘制的 painter。
final CustomPainter? foregroundPainter;

/// The size that this [CustomPaint] should aim for, given the layout
/// constraints, if there is no child.
///
/// Defaults to [Size.zero].
///
/// If there's a child, this is ignored, and the size of the child is used
/// instead.
// 在没有子 widget 时,根据布局约束,此 [CustomPaint] 应尝试达到的尺寸。
//
// 默认为 [Size.zero]。
//
// 如果有子 widget,则忽略此值,改用子 widget 的尺寸。
final Size size;

/// Whether the painting is complex enough to benefit from caching.
///
/// The compositor contains a raster cache that holds bitmaps of layers in
/// order to avoid the cost of repeatedly rendering those layers on each
/// frame. If this flag is not set, then the compositor will apply its own
/// heuristics to decide whether the layer containing this widget is complex
/// enough to benefit from caching.
///
/// This flag can't be set to true if both [painter] and [foregroundPainter]
/// are null because this flag will be ignored in such case.
// 绘制内容是否足够复杂,以至于值得使用缓存。
//
// 合成器(compositor)包含一个光栅缓存(raster cache),用于保存各层的位图,
// 以避免在每一帧中重复渲染这些层的开销。若未设置此标志,合成器将使用自身的
// 启发式规则来判断包含此 widget 的层是否足够复杂、值得缓存。
//
// 若 [painter] 和 [foregroundPainter] 均为 null,则不能将此标志设为 true,
// 因为在这种情况下该标志会被忽略。

final bool isComplex;

/// Whether the raster cache should be told that this painting is likely
/// to change in the next frame.
///
/// This hint tells the compositor not to cache the layer containing this
/// widget because the cache will not be used in the future. If this hint is
/// not set, the compositor will apply its own heuristics to decide whether
/// the layer is likely to be reused in the future.
///
/// This flag can't be set to true if both [painter] and [foregroundPainter]
/// are null because this flag will be ignored in such case.
//
// 是否告知光栅缓存:此绘制内容在下一帧很可能发生变化。
//
// 此提示告诉合成器不要缓存该层,因为缓存在将来不会被复用。
// 若未设置,合成器会用自身启发式规则判断该层是否可能被复用。
//
// 若 [painter] 和 [foregroundPainter] 均为 null,则不能设为 true(会被忽略)。
final bool willChange;

@override
RenderCustomPaint createRenderObject(BuildContext context) {
return RenderCustomPaint(
painter: painter,
foregroundPainter: foregroundPainter,
preferredSize: size,
isComplex: isComplex,
willChange: willChange,
);
}

@override
void updateRenderObject(BuildContext context, RenderCustomPaint renderObject) {
renderObject
..painter = painter
..foregroundPainter = foregroundPainter
..preferredSize = size
..isComplex = isComplex
..willChange = willChange;
}

@override
void didUnmountRenderObject(RenderCustomPaint renderObject) {
renderObject
..painter = null
..foregroundPainter = null;
}
}


  • painter: 背景画笔,会显示在子节点后面;
  • foregroundPainter: 前景画笔,会显示在子节点前面
  • size:当child为null时,代表默认绘制区域大小,如果有child则忽略此参数,画布尺寸则为child尺寸。如果有child但是想指定画布为特定大小,可以使用SizeBox包裹CustomPaint实现。
  • isComplex:是否复杂的绘制,如果是,Flutter会应用一些缓存策略来减少重复渲染的开销。
  • willChange:和isComplex配合使用,当启用缓存时,该属性代表在下一帧中绘制是否会改变。

ok,下面是最简单的代码:

点击显示main.dart代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

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

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title)),
body: Center(
child: CustomPaint(
painter: MyPainter(color: Colors.red),
child: Text('child child child \n child'), // 可选:先画 painter,再画 child
),
),
);
}
}

class MyPainter extends CustomPainter {
MyPainter({this.color = Colors.blue});
final Color color;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color;
canvas.drawRect(Offset.zero & size, paint);
}

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


翻译翻译: 新建了一个MyPainter继承自抽象类CustomPainter,重写paintshouldRepaint即可,然后就可以在paint中绘制你需要的自定义视图了.
下面来个相对较为复杂的使用:

点击显示main.dart代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import 'dart:math' as math;

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

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

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _progress;

@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 4))..repeat();
_progress = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
}

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

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title)),
body: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: SizedBox(
width: 320,
height: 220,
child: CustomPaint(
isComplex: true,
willChange: true,
painter: MyPainter(
progress: _progress,
baseColor: Colors.indigo,
accentColor: Colors.cyanAccent,
showGrid: true,
),
foregroundPainter: MyForegroundPainter(progress: _progress),
child: const Padding(
padding: EdgeInsets.all(24),
child: Align(
alignment: Alignment.bottomLeft,
child: Text(
'CustomPaint Demo\n背景 + 子组件 + 前景',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600, height: 1.4),
),
),
),
),
),
),
),
);
}
}

class MyPainter extends CustomPainter {
MyPainter({
required this.progress,
this.baseColor = Colors.indigo,
this.accentColor = Colors.cyanAccent,
this.showGrid = true,
}) : super(repaint: progress);

final Listenable progress;
final Color baseColor;
final Color accentColor;
final bool showGrid;

double get _t => progress is Animation<double> ? (progress as Animation<double>).value : 0;

@override
void paint(Canvas canvas, Size size) {
final t = _t;
final bgRect = Offset.zero & size;

final bgPaint = Paint()
..shader = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color.lerp(baseColor, accentColor, t * 0.6)!,
Color.lerp(baseColor.withValues(alpha: 0.4), accentColor, t)!,
],
).createShader(bgRect);
canvas.drawRect(bgRect, bgPaint);

if (showGrid) {
_drawGrid(canvas, size, t);
}
_drawWave(canvas, size, t);
_drawRings(canvas, size, t);
}

void _drawGrid(Canvas canvas, Size size, double t) {
const step = 24.0;
final gridPaint = Paint()
..color = Colors.white.withValues(alpha: 0.08)
..strokeWidth = 1;

final offset = (t * step) % step;
for (double x = -offset; x <= size.width; x += step) {
canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
}
for (double y = -offset; y <= size.height; y += step) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
}
}

void _drawWave(Canvas canvas, Size size, double t) {
final path = Path();
final waveHeight = size.height * 0.12;
final baseY = size.height * 0.65;

path.moveTo(0, baseY);
for (double x = 0; x <= size.width; x += 2) {
final y = baseY + math.sin((x / size.width * 2 * math.pi) + (t * 2 * math.pi)) * waveHeight;
path.lineTo(x, y);
}
path
..lineTo(size.width, size.height)
..lineTo(0, size.height)
..close();

final wavePaint = Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [accentColor.withValues(alpha: 0.35), accentColor.withValues(alpha: 0.05)],
).createShader(Offset.zero & size);

canvas.drawPath(path, wavePaint);
}

void _drawRings(Canvas canvas, Size size, double t) {
final center = Offset(size.width * 0.75, size.height * 0.28);
for (int i = 0; i < 3; i++) {
final radius = 30.0 + i * 18 + t * 12;
final ringPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.5 - i * 0.5
..color = Colors.white.withValues(alpha: 0.25 - i * 0.05);
canvas.drawCircle(center, radius, ringPaint);
}
}

@override
bool shouldRepaint(covariant MyPainter oldDelegate) {
return oldDelegate.baseColor != baseColor ||
oldDelegate.accentColor != accentColor ||
oldDelegate.showGrid != showGrid ||
oldDelegate.progress != progress;
}
}

class MyForegroundPainter extends CustomPainter {
MyForegroundPainter({required this.progress}) : super(repaint: progress);

final Listenable progress;

double get _t => progress is Animation<double> ? (progress as Animation<double>).value : 0;

@override
void paint(Canvas canvas, Size size) {
final scanY = size.height * _t;
final scanPaint = Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.white.withValues(alpha: 0.15), Colors.transparent],
stops: const [0.0, 0.5, 1.0],
).createShader(Rect.fromLTWH(0, scanY - 20, size.width, 40));

canvas.drawRect(Rect.fromLTWH(0, scanY - 20, size.width, 40), scanPaint);

final borderPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = Colors.white.withValues(alpha: 0.35);
canvas.drawRRect(
RRect.fromRectAndRadius(Rect.fromLTWH(1, 1, size.width - 2, size.height - 2), const Radius.circular(16)),
borderPaint,
);
}

@override
bool shouldRepaint(covariant MyForegroundPainter oldDelegate) => oldDelegate.progress != progress;
}

复杂实现




在 Flutter 中,自定义 RenderObject 是直接进入 Flutter 渲染层(Rendering Layer)的方式,用于实现一些 Widget / RenderBox 无法方便完成的复杂布局和绘制效果。

典型场景:

  • 自定义布局算法(例如瀑布流、特殊排列)
  • 高性能绘制(避免大量 Widget)
  • 自定义命中测试(HitTest)
  • 自定义绘制流程
  • 类似 CustomPaint 但需要控制布局、子节点管理时

ok.先从最简单的开始摸索:

点击显示main.dart代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154

import 'package:custom_render_object/MyStack_hitTest.dart';
import 'package:custom_render_object/exmaple/particle_controller.dart';
import 'package:custom_render_object/exmaple/particle_field.dart';
import 'package:custom_render_object/exmaple/render_particle_field.dart';
import 'package:custom_render_object/my_box.dart';
import 'package:custom_render_object/my_container.dart';
import 'package:custom_render_object/my_row.dart';
import 'package:custom_render_object/my_stack.dart';
import 'package:custom_render_object/MyStack_hitTest.dart';
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';

void main() {
runApp(const MyApp());
}

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

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
late ParticleController controller;

@override
void initState() {
super.initState();

controller = ParticleController(
count: 1500,
onUpdate: () {
final render = key.currentContext?.findRenderObject() as RenderParticleField?;

render?.markNeedsPaint();
},
);
}

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

final GlobalKey key = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title)),
body: Center(
// child: MyBox(),
// child: MyContainer(
// padding: EdgeInsets.all(200),
// color: Colors.blue,
// child: Text("Hello RenderObject", style: TextStyle(color: Colors.white)),
// ),
// child: MyRow(
// spacing: 20,

// children: const [
// Text("A", style: TextStyle(fontSize: 40)),

// Text("B", style: TextStyle(fontSize: 40)),

// Text("C", style: TextStyle(fontSize: 40)),
// ],
// ),
// child: MyStack(
// children: [
// MyPositioned(left: 0, top: 0, child: Container(width: 150, height: 150, color: Colors.red)),

// MyPositioned(left: 80, top: 80, child: Container(width: 150, height: 150, color: Colors.blue)),

// MyPositioned(left: 160, top: 160, child: Container(width: 100, height: 100, color: Colors.green)),
// ],
// ),
// child: MyHitTestStack(
// children: [
// Listener(
// onPointerDown: (_) {
// debugPrint("RED CLICKED");
// },
// child: Container(width: 220, height: 220, color: Colors.red),
// ),
// Listener(
// behavior: HitTestBehavior.translucent,
// onPointerDown: (_) {
// debugPrint("BLUE CLICKED");
// },
// child: Container(width: 160, height: 160, color: Colors.blue),
// ),
// Listener(
// onPointerDown: (_) {
// debugPrint("GREEN CLICKED");
// },
// child: Container(width: 100, height: 100, color: Colors.green),
// ),
// ],
// ),
// child: SizedBox(
// width: 400,
// height: 120,
// child: TimelineTrack(
// pixelsPerHour: 50,

// children: const [
// TimelineItem(start: 1, end: 3, label: "Meeting A", child: Text("Meeting A")),

// TimelineItem(start: 4, end: 6, label: "Design", child: Text("Design")),

// TimelineItem(start: 7, end: 9, label: "Review", child: Text("Review")),
// ],
// ),
// ),
child: Container(
width: 800,
height: 800,
color: Colors.pink,
child: Listener(
onPointerMove: (e) {
final render = key.currentContext?.findRenderObject() as RenderParticleField?;

if (render == null) return;

controller.updatePointer(e.localPosition, render.size);
},
child: ParticleField(key: key, controller: controller),
),
),
),
);
}
}



1、从建个长宽200X100的红色框开始
点击显示my_box.dart代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import 'package:flutter/material.dart';

class MyBox extends LeafRenderObjectWidget {
const MyBox({super.key});

@override
RenderObject createRenderObject(BuildContext context) {
return RenderMyBox();
}
}

class RenderMyBox extends RenderBox {
@override
void performLayout() {
size = const Size(200, 100);
}

@override
void paint(PaintingContext context, Offset offset) {
context.canvas.drawRect(offset & size, Paint()..color = Colors.red);
}
}

renderObject_MyBox.png

2、建个container

点击显示my_container.dart代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/*
* @Author: shaoting0730 510738319@qq.com
* @Date: 2026-06-23 11:02:12
* @LastEditors: shaoting0730 510738319@qq.com
* @LastEditTime: 2026-06-23 11:05:00
* @FilePath: /custom_render_object/lib/my_container.dart
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';


// ================================
// Widget 层
// ================================

class MyContainer extends SingleChildRenderObjectWidget {
final EdgeInsets padding;

final Color color;

const MyContainer({super.key, this.padding = EdgeInsets.zero, this.color = Colors.blue, Widget? child}) : super(child: child);

@override
RenderObject createRenderObject(BuildContext context) {
return RenderMyContainer(padding, color);
}

@override
void updateRenderObject(BuildContext context, RenderMyContainer renderObject) {
renderObject
..padding = padding
..color = color;
}
}

// ================================
// RenderObject 层
// ================================

class RenderMyContainer extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
EdgeInsets _padding;

Color _color;

RenderMyContainer(this._padding, this._color);

set padding(EdgeInsets value) {
if (_padding != value) {
_padding = value;

markNeedsLayout();
}
}

set color(Color value) {
if (_color != value) {
_color = value;

markNeedsPaint();
}
}

// -------------------------------
// Layout
// -------------------------------

@override
void performLayout() {
if (child != null) {
child!.layout(constraints.deflate(_padding), parentUsesSize: true);

final childData = child!.parentData as BoxParentData;

childData.offset = Offset(_padding.left, _padding.top);
}

final childSize = child?.size ?? Size.zero;

size = constraints.constrain(Size(childSize.width + _padding.horizontal, childSize.height + _padding.vertical));
}

// -------------------------------
// Paint
// -------------------------------

@override
void paint(PaintingContext context, Offset offset) {
// 1. 绘制自己的背景

context.canvas.drawRect(offset & size, Paint()..color = _color);

// 2. 绘制 child

if (child != null) {
final childData = child!.parentData as BoxParentData;

context.paintChild(child!, offset + childData.offset);
}
}
}

renderObject_MyContainer
3、建个row
点击显示my_row.dart代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

// ============================
// Widget
// ============================

class MyRow extends MultiChildRenderObjectWidget {
final double spacing;

MyRow({super.key, this.spacing = 0, required List<Widget> children}) : super(children: children);

@override
RenderObject createRenderObject(BuildContext context) {
return RenderMyRow(spacing);
}
}

// ============================
// ParentData
// 保存 child 布局信息
// ============================

class MyRowParentData extends ContainerBoxParentData<RenderBox> {}

// ============================
// RenderObject
// ============================

class RenderMyRow extends RenderBox with ContainerRenderObjectMixin<RenderBox, MyRowParentData>, RenderBoxContainerDefaultsMixin<RenderBox, MyRowParentData> {
double spacing;

RenderMyRow(this.spacing);

// 创建 ParentData

@override
void setupParentData(RenderBox child) {
if (child.parentData is! MyRowParentData) {
child.parentData = MyRowParentData();
}
}

// ==========================
// Layout
// ==========================

@override
void performLayout() {
double width = 0;

double height = 0;

RenderBox? child = firstChild;

// 第一遍:
// 测量所有 child

while (child != null) {
child.layout(constraints.loosen(), parentUsesSize: true);

final childSize = child.size;

width += childSize.width;

height = height > childSize.height ? height : childSize.height;

child = childAfter(child);
}

// 加 spacing

final count = childCount;

width += spacing * (count - 1);

size = constraints.constrain(Size(width, height));

// 第二遍:
// 设置 child 位置

double dx = 0;

child = firstChild;

while (child != null) {
final parentData = child.parentData as MyRowParentData;

parentData.offset = Offset(dx, 0);

dx += child.size.width;

dx += spacing;

child = childAfter(child);
}
}

// ==========================
// Paint
// ==========================

@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
}

renderObject_MyRow

4、建个Stack

点击显示my_stack.dart代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

//================================================
// Stack Widget
//================================================

class MyStack extends MultiChildRenderObjectWidget {
const MyStack({super.key, required super.children});

@override
RenderObject createRenderObject(BuildContext context) {
return RenderMyStack();
}
}

//================================================
// Positioned Widget
//================================================

class MyPositioned extends ParentDataWidget<MyStackParentData> {
final double left;

final double top;

const MyPositioned({super.key, required this.left, required this.top, required super.child});

@override
void applyParentData(RenderObject renderObject) {
final data = renderObject.parentData as MyStackParentData;

bool needsLayout = false;

if (data.left != left) {
data.left = left;

needsLayout = true;
}

if (data.top != top) {
data.top = top;

needsLayout = true;
}

if (needsLayout) {
final parent = renderObject.parent;

if (parent is RenderObject) {
parent.markNeedsLayout();
}
}
}

@override
Type get debugTypicalAncestorWidgetClass => MyStack;
}

//================================================
// ParentData
// 保存 child 位置信息
//================================================

class MyStackParentData extends ContainerBoxParentData<RenderBox> {
double left = 0;

double top = 0;
}

//================================================
// RenderObject
//================================================

class RenderMyStack extends RenderBox
with ContainerRenderObjectMixin<RenderBox, MyStackParentData>, RenderBoxContainerDefaultsMixin<RenderBox, MyStackParentData> {
// 创建 ParentData

@override
void setupParentData(RenderBox child) {
if (child.parentData is! MyStackParentData) {
child.parentData = MyStackParentData();
}
}

//===========================
// Layout
//===========================

@override
void performLayout() {
double width = 0;

double height = 0;

RenderBox? child = firstChild;

while (child != null) {
// 让 child 自己决定大小

child.layout(constraints.loosen(), parentUsesSize: true);

final data = child.parentData as MyStackParentData;

width = width > data.left + child.size.width ? width : data.left + child.size.width;

height = height > data.top + child.size.height ? height : data.top + child.size.height;

child = childAfter(child);
}

// Stack 自己大小

size = constraints.constrain(Size(width, height));
}

//===========================
// Paint
//===========================

@override
void paint(PaintingContext context, Offset offset) {
RenderBox? child = firstChild;

while (child != null) {
final data = child.parentData as MyStackParentData;

context.paintChild(child, offset + Offset(data.left, data.top));

child = childAfter(child);
}
}
}
renderObject_MyStack
4、建个HitTest点击
点击显示MyStack_hitTest.dart代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

//================================================
// Stack Widget
//================================================

class MyHitTestStack extends MultiChildRenderObjectWidget {
const MyHitTestStack({super.key, required super.children});

@override
RenderObject createRenderObject(BuildContext context) {
return RenderMyHitTestStack();
}
}

//================================================
// ParentData
//================================================

class MyHitTestStackParentData extends ContainerBoxParentData<RenderBox> {
double left = 0;
double top = 0;

String name = "";
}

//================================================
// RenderStack(核心)
//================================================

class RenderMyHitTestStack extends RenderBox
with ContainerRenderObjectMixin<RenderBox, MyHitTestStackParentData>, RenderBoxContainerDefaultsMixin<RenderBox, MyHitTestStackParentData> {
@override
void setupParentData(RenderBox child) {
if (child.parentData is! MyHitTestStackParentData) {
child.parentData = MyHitTestStackParentData();
}
}

//========================
// Layout
//========================

@override
void performLayout() {
double maxW = 0;
double maxH = 0;

RenderBox? child = firstChild;

while (child != null) {
child.layout(constraints.loosen(), parentUsesSize: true);

final data = child.parentData as MyHitTestStackParentData;

maxW = maxW > data.left + child.size.width ? maxW : data.left + child.size.width;

maxH = maxH > data.top + child.size.height ? maxH : data.top + child.size.height;

child = childAfter(child);
}

size = constraints.constrain(Size(maxW, maxH));
}

//========================
// Paint
//========================

@override
void paint(PaintingContext context, Offset offset) {
RenderBox? child = firstChild;

while (child != null) {
final data = child.parentData as MyHitTestStackParentData;

context.paintChild(child, offset + Offset(data.left, data.top));

child = childAfter(child);
}
}

//========================
// HIT TEST(收集命中)
//========================

@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
RenderBox? child = lastChild;

bool hit = false;

while (child != null) {
final data = child.parentData as MyHitTestStackParentData;

final childPosition = position - Offset(data.left, data.top);

if (child.hitTest(result, position: childPosition)) {
hit = true;
}

child = childBefore(child);
}

return hit;
}

//========================
// POINTER EVENT(关键)
//========================

@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
if (event is PointerDownEvent) {
_dispatchTap(entry, event.position);
}
}

//========================
// 手动广播点击
//========================

void _dispatchTap(HitTestEntry entry, Offset globalPosition) {
RenderBox? child = firstChild;

while (child != null) {
final data = child.parentData as MyHitTestStackParentData;

final rect = Offset(data.left, data.top) & child.size;

if (rect.contains(globalPosition)) {
debugPrint("${data.name} CLICKED");
}

child = childAfter(child);
}
}
}

renderObject_MyHitTestStack


接下来,来个真正应该使用RenderObject自定义的例子.
点击显示particle.dart代码
1
2
3
4
5
6
7
8
9
10
import 'package:flutter/material.dart';

class Particle {
Offset pos;
Offset vel;
double brightness;

Particle(this.pos, this.vel, this.brightness);
}

点击显示particle_controller.dart代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import 'dart:math';
import 'dart:ui';
import 'package:flutter/scheduler.dart';
import 'particle.dart';

class ParticleController {
ParticleController({required int count, required this.onUpdate}) {
_init(count);
_ticker = Ticker(_tick)..start();
}

final VoidCallback onUpdate;

final List<Particle> particles = [];
final Random _random = Random();

late final Ticker _ticker;

Offset pointer = Offset.zero;
bool hasPointer = false;

double _time = 0;

void _init(int count) {
particles.clear();

for (int i = 0; i < count; i++) {
particles.add(
Particle(
Offset(_random.nextDouble(), _random.nextDouble()),
Offset((_random.nextDouble() - 0.5) * 0.002, (_random.nextDouble() - 0.5) * 0.002),
_random.nextDouble(),
),
);
}
}

void _tick(Duration _) {
_time += 0.016;
_update();
onUpdate();
}

void _update() {
for (final p in particles) {
p.pos += p.vel;

if (p.pos.dx < 0 || p.pos.dx > 1) p.vel = Offset(-p.vel.dx, p.vel.dy);
if (p.pos.dy < 0 || p.pos.dy > 1) p.vel = Offset(p.vel.dx, -p.vel.dy);

if (hasPointer) {
final dx = p.pos.dx - pointer.dx;
final dy = p.pos.dy - pointer.dy;

final d = sqrt(dx * dx + dy * dy);

if (d < 0.2) {
p.vel += Offset(dx, dy) * -0.0005;
p.brightness = 1.0;
} else {
p.brightness *= 0.98;
}
}

p.brightness = (p.brightness + 0.01).clamp(0.2, 1.0);
}
}

void updatePointer(Offset local, Size size) {
if (size == Size.zero) return;

pointer = Offset(local.dx / size.width, local.dy / size.height);

hasPointer = true;
}

void dispose() {
_ticker.dispose();
}
}

点击显示particle_field.dart代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'particle_controller.dart';
import 'render_particle_field.dart';

class ParticleField extends LeafRenderObjectWidget {
final ParticleController controller;

const ParticleField({super.key, required this.controller});

@override
RenderObject createRenderObject(BuildContext context) {
return RenderParticleField(controller: controller);
}

@override
void updateRenderObject(BuildContext context, RenderParticleField renderObject) {
renderObject.controller = controller;
}
}

点击显示render_particle_field.dart代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
import 'particle_controller.dart';

class RenderParticleField extends RenderBox {
RenderParticleField({required ParticleController controller}) : _controller = controller;

ParticleController _controller;

set controller(ParticleController c) {
_controller = c;
markNeedsPaint();
}

@override
void performLayout() {
size = constraints.biggest;
}

@override
void paint(PaintingContext context, Offset offset) {
final canvas = context.canvas;

final size = this.size;

for (final p in _controller.particles) {
final pos = Offset(offset.dx + p.pos.dx * size.width, offset.dy + p.pos.dy * size.height);

final paint = Paint()
..color = Colors.white.withOpacity(p.brightness)
..style = PaintingStyle.fill;

canvas.drawCircle(pos, 2 + p.brightness * 2, paint);
}
}

@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
return true;
}
}

renderObject_exmaple

ok、以上就是在AI的帮助下,学习下renderObject的大致历程.

源码1
源码2

🔈大师兄, AI加炒菜, 有没有搞头~~~

AI淘汰我, 有搞头…..

参考文献: openai
参考文献: Anthropic
参考文献: deepseek
参考文献: Google DeepMind