custom_layout.dart
9.34 KB
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import '../constants.dart';
import '../utils/get_type_of.dart';
import '../utils/render_box_offset.dart';
import '../utils/render_box_layout.dart';
abstract class CustomLayoutDelegate<T> {
const CustomLayoutDelegate();
// childrenTable parameter is for a hack to render asynchronously for flutter_svg
Size computeLayout(
BoxConstraints constraints,
Map<T, RenderBox> childrenTable, {
bool dry,
});
double getIntrinsicSize({
required Axis sizingDirection,
required bool max,
required double
extent, // the extent in the direction that isn't the sizing direction
required double Function(RenderBox child, double extent)
childSize, // a method to find the size in the sizing direction);
required Map<T, RenderBox> childrenTable,
});
double? computeDistanceToActualBaseline(
TextBaseline baseline, Map<T, RenderBox> childrenTable);
void additionalPaint(PaintingContext context, Offset offset) {}
}
class CustomLayoutParentData<T> extends ContainerBoxParentData<RenderBox> {
/// An object representing the identity of this child.
T? id;
@override
String toString() => '${super.toString()}; id=$id';
}
class CustomLayoutId<T> extends ParentDataWidget<CustomLayoutParentData<T>> {
/// Marks a child with a layout identifier.
///
/// Both the child and the id arguments must not be null.
CustomLayoutId({
Key? key,
required this.id,
required Widget child,
}) : assert(id != null),
super(key: key ?? ValueKey<T>(id), child: child);
final T id;
@override
void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is CustomLayoutParentData);
final parentData = renderObject.parentData as CustomLayoutParentData;
if (parentData.id != id) {
parentData.id = id;
final targetParent = renderObject.parent;
if (targetParent is RenderObject) targetParent.markNeedsLayout();
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<T>('id', id));
}
@override
Type get debugTypicalAncestorWidgetClass => getTypeOf<CustomLayout<T>>();
}
class CustomLayout<T> extends MultiChildRenderObjectWidget {
/// Creates a custom multi-child layout.
///
/// The [delegate] argument must not be null.
CustomLayout({
Key? key,
required this.delegate,
required List<Widget> children,
}) : super(key: key, children: children);
/// The delegate that controls the layout of the children.
final CustomLayoutDelegate<T> delegate;
@override
RenderCustomLayout<T> createRenderObject(BuildContext context) =>
RenderCustomLayout<T>(delegate: delegate);
@override
void updateRenderObject(
BuildContext context, RenderCustomLayout renderObject) {
renderObject.delegate = delegate;
}
}
class RenderCustomLayout<T> extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, CustomLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, CustomLayoutParentData> {
RenderCustomLayout({
List<RenderBox>? children,
required CustomLayoutDelegate<T> delegate,
}) : _delegate = delegate {
addAll(children);
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! CustomLayoutParentData<T>) {
child.parentData = CustomLayoutParentData<T>();
}
}
/// The delegate that controls the layout of the children.
CustomLayoutDelegate<T> get delegate => _delegate;
CustomLayoutDelegate<T> _delegate;
set delegate(CustomLayoutDelegate<T> newDelegate) {
if (_delegate != newDelegate) {
markNeedsLayout();
}
_delegate = newDelegate;
}
Map<T, RenderBox> get childrenTable {
final res = <T, RenderBox>{};
var child = firstChild;
while (child != null) {
final childParentData = child.parentData as CustomLayoutParentData<T>;
assert(() {
if (childParentData.id == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Every child of a RenderCustomLayout must have an ID '
'in its parent data.'),
child!.describeForError('The following child has no ID'),
]);
}
if (res.containsKey(childParentData.id)) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Every child of a RenderCustomLayout must have a unique ID.'),
child!.describeForError(
'The following child has a ID of ${childParentData.id}'),
res[childParentData.id!]!
.describeForError('While the following child has the same ID')
]);
}
return true;
}());
res[childParentData.id!] = child;
child = childParentData.nextSibling;
}
return res;
}
@override
double computeMinIntrinsicWidth(double height) => delegate.getIntrinsicSize(
sizingDirection: Axis.horizontal,
max: false,
extent: height,
childSize: (RenderBox child, double extent) =>
child.getMinIntrinsicWidth(extent),
childrenTable: childrenTable);
@override
double computeMaxIntrinsicWidth(double height) => delegate.getIntrinsicSize(
sizingDirection: Axis.horizontal,
max: true,
extent: height,
childSize: (RenderBox child, double extent) =>
child.getMaxIntrinsicWidth(extent),
childrenTable: childrenTable);
@override
double computeMinIntrinsicHeight(double width) => delegate.getIntrinsicSize(
sizingDirection: Axis.vertical,
max: false,
extent: width,
childSize: (RenderBox child, double extent) =>
child.getMinIntrinsicHeight(extent),
childrenTable: childrenTable);
@override
double computeMaxIntrinsicHeight(double width) => delegate.getIntrinsicSize(
sizingDirection: Axis.vertical,
max: true,
extent: width,
childSize: (RenderBox child, double extent) =>
child.getMaxIntrinsicHeight(extent),
childrenTable: childrenTable);
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) =>
delegate.computeDistanceToActualBaseline(baseline, childrenTable);
@override
void performLayout() {
this.size = _computeLayout(constraints, dry: false);
}
Size computeDryLayout(BoxConstraints constraints) =>
_computeLayout(constraints);
Size _computeLayout(BoxConstraints constraints, {bool dry = true}) =>
constraints.constrain(
delegate.computeLayout(constraints, childrenTable, dry: dry),
);
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
delegate.additionalPaint(context, offset);
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) =>
defaultHitTestChildren(result, position: position);
}
class AxisConfiguration<T> {
final double size;
final Map<T, double> offsetTable;
AxisConfiguration({
required this.size,
required this.offsetTable,
});
}
abstract class IntrinsicLayoutDelegate<T> extends CustomLayoutDelegate<T> {
const IntrinsicLayoutDelegate();
AxisConfiguration<T> performHorizontalIntrinsicLayout({
required Map<T, double> childrenWidths,
bool isComputingIntrinsics = false,
});
AxisConfiguration<T> performVerticalIntrinsicLayout({
required Map<T, double> childrenHeights,
required Map<T, double> childrenBaselines,
bool isComputingIntrinsics = false,
});
@override
double getIntrinsicSize({
required Axis sizingDirection,
required bool max,
required double extent,
required double Function(RenderBox child, double extent) childSize,
required Map<T, RenderBox> childrenTable,
}) {
if (sizingDirection == Axis.horizontal) {
return performHorizontalIntrinsicLayout(
childrenWidths: childrenTable.map(
(key, value) => MapEntry(key, childSize(value, double.infinity))),
isComputingIntrinsics: true,
).size;
} else {
final childrenHeights = childrenTable.map(
(key, value) => MapEntry(key, childSize(value, double.infinity)));
return performVerticalIntrinsicLayout(
childrenHeights: childrenHeights,
childrenBaselines: childrenHeights,
isComputingIntrinsics: true,
).size;
}
}
@override
Size computeLayout(
BoxConstraints constraints,
Map<T, RenderBox> childrenTable, {
bool dry = true,
}) {
final sizeMap = <T, Size>{};
for (final childEntry in childrenTable.entries) {
sizeMap[childEntry.key] =
childEntry.value.getLayoutSize(infiniteConstraint, dry: dry);
}
final hconf = performHorizontalIntrinsicLayout(
childrenWidths:
sizeMap.map((key, value) => MapEntry(key, value.width)));
final vconf = performVerticalIntrinsicLayout(
childrenHeights: sizeMap.map((key, value) => MapEntry(key, value.height)),
childrenBaselines: childrenTable.map((key, value) => MapEntry(
key,
dry
? 0
: value.getDistanceToBaseline(TextBaseline.alphabetic,
onlyReal: true)!,
)),
);
if (!dry) {
childrenTable.forEach((id, child) => child.offset =
Offset(hconf.offsetTable[id]!, vconf.offsetTable[id]!));
}
return Size(hconf.size, vconf.size);
}
}