Skip to content

Commit f9a578d

Browse files
authored
An example of parentData usage. (#131818)
1 parent e972d5a commit f9a578d

File tree

4 files changed

+447
-18
lines changed

4 files changed

+447
-18
lines changed
Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter/rendering.dart';
7+
8+
void main() => runApp(const SampleApp());
9+
10+
class SampleApp extends StatefulWidget {
11+
const SampleApp({super.key});
12+
13+
@override
14+
State<SampleApp> createState() => _SampleAppState();
15+
}
16+
17+
class _SampleAppState extends State<SampleApp> {
18+
// This can be toggled using buttons in the UI to change which layout render object is used.
19+
bool _compact = false;
20+
21+
// This is the content we show in the rendering.
22+
//
23+
// Headline and Paragraph are simple custom widgets defined below.
24+
//
25+
// Any widget _could_ be specified here, and would render fine.
26+
// The Headline and Paragraph widgets are used so that the renderer
27+
// can distinguish between the kinds of content and use different
28+
// spacing between different children.
29+
static const List<Widget> body = <Widget>[
30+
Headline('Bugs that improve T for future bugs'),
31+
Paragraph(
32+
'The best bugs to fix are those that make us more productive '
33+
'in the future. Reducing test flakiness, reducing technical '
34+
'debt, increasing the number of team members who are able to '
35+
'review code confidently and well: this all makes future bugs '
36+
'easier to fix, which is a huge multiplier to our overall '
37+
'effectiveness and thus to developer happiness.',
38+
),
39+
Headline('Bugs affecting more people are more valuable (maximize N)'),
40+
Paragraph(
41+
'We will make more people happier if we fix a bug experienced by more people.'
42+
),
43+
Paragraph(
44+
'One thing to be careful about is to think about the number of '
45+
'people we are ignoring in our metrics. For example, if we had '
46+
'a bug that prevented our product from working on Windows, we '
47+
'would have no Windows users, so the bug would affect nobody. '
48+
'However, fixing the bug would enable millions of developers '
49+
"to use our product, and that's the number that counts."
50+
),
51+
Headline('Bugs with greater impact on developers are more valuable (maximize ΔH)'),
52+
Paragraph(
53+
'A slight improvement to the user experience is less valuable '
54+
'than a greater improvement. For example, if our application, '
55+
'under certain conditions, shows a message with a typo, and '
56+
'then crashes because of an off-by-one error in the code, '
57+
'fixing the crash is a higher priority than fixing the typo.'
58+
),
59+
];
60+
61+
// This is the description of the demo's interface.
62+
@override
63+
Widget build(BuildContext context) {
64+
return MaterialApp(
65+
home: Scaffold(
66+
appBar: AppBar(
67+
title: const Text('Custom Render Boxes'),
68+
// There are two buttons over to the top right of the demo that let you
69+
// toggle between the two rendering modes.
70+
actions: <Widget>[
71+
IconButton(
72+
icon: const Icon(Icons.density_small),
73+
isSelected: _compact,
74+
onPressed: () {
75+
setState(() { _compact = true; });
76+
},
77+
),
78+
IconButton(
79+
icon: const Icon(Icons.density_large),
80+
isSelected: !_compact,
81+
onPressed: () {
82+
setState(() { _compact = false; });
83+
},
84+
),
85+
],
86+
),
87+
body: SingleChildScrollView(
88+
padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 20.0),
89+
// CompactLayout and OpenLayout are the two rendering widgets defined below.
90+
child: _compact ? const CompactLayout(children: body) : const OpenLayout(children: body),
91+
),
92+
),
93+
);
94+
}
95+
}
96+
97+
// Headline and Paragraph are just wrappers around the Text widget, but they
98+
// also introduce a TextCategory widget that the CompactLayout and OpenLayout
99+
// widgets can read to determine what kind of child is being rendered.
100+
101+
class Headline extends StatelessWidget {
102+
const Headline(this.text, { super.key });
103+
104+
final String text;
105+
106+
@override
107+
Widget build(BuildContext context) {
108+
return TextCategory(
109+
category: 'headline',
110+
child: Text(text, style: Theme.of(context).textTheme.titleLarge),
111+
);
112+
}
113+
}
114+
115+
class Paragraph extends StatelessWidget {
116+
const Paragraph(this.text, { super.key });
117+
118+
final String text;
119+
120+
@override
121+
Widget build(BuildContext context) {
122+
return TextCategory(
123+
category: 'paragraph',
124+
child: Text(text, style: Theme.of(context).textTheme.bodyLarge),
125+
);
126+
}
127+
}
128+
129+
// This is the ParentDataWidget that allows us to specify what kind of child
130+
// is being rendered. It allows information to be shared with the render object
131+
// without violating the principle of agnostic composition (wherein parents should
132+
// work with any child, not only support a fixed set of children).
133+
class TextCategory extends ParentDataWidget<TextFlowParentData> {
134+
const TextCategory({ super.key, required this.category, required super.child });
135+
136+
final String category;
137+
138+
@override
139+
void applyParentData(RenderObject renderObject) {
140+
final TextFlowParentData parentData = renderObject.parentData! as TextFlowParentData;
141+
if (parentData.category != category) {
142+
parentData.category = category;
143+
renderObject.parent!.markNeedsLayout();
144+
}
145+
}
146+
147+
@override
148+
Type get debugTypicalAncestorWidgetClass => OpenLayout;
149+
}
150+
151+
// This is one of the two layout variants. It is a widget that defers to
152+
// a render object defined below (RenderCompactLayout).
153+
class CompactLayout extends MultiChildRenderObjectWidget {
154+
const CompactLayout({ super.key, super.children });
155+
156+
@override
157+
RenderCompactLayout createRenderObject(BuildContext context) {
158+
return RenderCompactLayout();
159+
}
160+
161+
@override
162+
void updateRenderObject(BuildContext context, RenderCompactLayout renderObject) {
163+
// nothing to update
164+
}
165+
}
166+
167+
// This is the other of the two layout variants. It is a widget that defers to a
168+
// render object defined below (RenderOpenLayout).
169+
class OpenLayout extends MultiChildRenderObjectWidget {
170+
const OpenLayout({ super.key, super.children });
171+
172+
@override
173+
RenderOpenLayout createRenderObject(BuildContext context) {
174+
return RenderOpenLayout();
175+
}
176+
177+
@override
178+
void updateRenderObject(BuildContext context, RenderOpenLayout renderObject) {
179+
// nothing to update
180+
}
181+
}
182+
183+
// This is the data structure that contains the kind of data that can be
184+
// passed to the parent to label the child. It is literally stored on
185+
// the RenderObject child, in its "parentData" field.
186+
class TextFlowParentData extends ContainerBoxParentData<RenderBox> {
187+
String category = '';
188+
}
189+
190+
// This is the bulk of the layout logic. (It's similar to RenderListBody,
191+
// but only supports vertical layout.) It has no properties.
192+
//
193+
// This is an abstract class that is then extended by RenderCompactLayout and
194+
// RenderOpenLayout to get different layouts based on the children's categories,
195+
// as stored in the ParentData structure defined above.
196+
//
197+
// The documentation for the RenderBox class and its members provides much
198+
// more detail on how to implement each of the methods below.
199+
abstract class RenderTextFlow extends RenderBox
200+
with ContainerRenderObjectMixin<RenderBox, TextFlowParentData>,
201+
RenderBoxContainerDefaultsMixin<RenderBox, TextFlowParentData> {
202+
RenderTextFlow({ List<RenderBox>? children }) {
203+
addAll(children);
204+
}
205+
206+
@override
207+
void setupParentData(RenderBox child) {
208+
if (child.parentData is! TextFlowParentData) {
209+
child.parentData = TextFlowParentData();
210+
}
211+
}
212+
213+
// This is the function that is overridden by the subclasses to do the
214+
// actual decision about the space to use between children.
215+
double spacingBetween(String before, String after);
216+
217+
// The next few functions are the layout functions. In each case we walk the
218+
// children, calling each one to determine the geometry of the child, and use
219+
// that to determine the layout.
220+
221+
// The first two functions compute the intrinsic width of the render object,
222+
// as seen when using the IntrinsicWidth widget.
223+
//
224+
// They essentially defer to the widest child.
225+
226+
@override
227+
double computeMinIntrinsicWidth(double height) {
228+
double width = 0.0;
229+
RenderBox? child = firstChild;
230+
while (child != null) {
231+
final double childWidth = child.getMinIntrinsicWidth(height);
232+
if (childWidth > width) {
233+
width = childWidth;
234+
}
235+
child = childAfter(child);
236+
}
237+
return width;
238+
}
239+
240+
@override
241+
double computeMaxIntrinsicWidth(double height) {
242+
double width = 0.0;
243+
RenderBox? child = firstChild;
244+
while (child != null) {
245+
final double childWidth = child.getMaxIntrinsicWidth(height);
246+
if (childWidth > width) {
247+
width = childWidth;
248+
}
249+
child = childAfter(child);
250+
}
251+
return width;
252+
}
253+
254+
// The next two functions compute the intrinsic height of the render object,
255+
// as seen when using the IntrinsicHeight widget.
256+
//
257+
// They add up the height contributed by each child.
258+
//
259+
// They have to take into account the categories of the children and the
260+
// spacing that will be added, hence the slightly more elaborate logic.
261+
262+
@override
263+
double computeMinIntrinsicHeight(double width) {
264+
String? previousCategory;
265+
double height = 0.0;
266+
RenderBox? child = firstChild;
267+
while (child != null) {
268+
final String category = (child.parentData! as TextFlowParentData).category;
269+
if (previousCategory != null) {
270+
height += spacingBetween(previousCategory, category);
271+
}
272+
height += child.getMinIntrinsicHeight(width);
273+
previousCategory = category;
274+
child = childAfter(child);
275+
}
276+
return height;
277+
}
278+
279+
@override
280+
double computeMaxIntrinsicHeight(double width) {
281+
String? previousCategory;
282+
double height = 0.0;
283+
RenderBox? child = firstChild;
284+
while (child != null) {
285+
final String category = (child.parentData! as TextFlowParentData).category;
286+
if (previousCategory != null) {
287+
height += spacingBetween(previousCategory, category);
288+
}
289+
height += child.getMaxIntrinsicHeight(width);
290+
previousCategory = category;
291+
child = childAfter(child);
292+
}
293+
return height;
294+
}
295+
296+
// This function implements the baseline logic. Because this class does
297+
// nothing special, we just defer to the default implementation in the
298+
// RenderBoxContainerDefaultsMixin utility class.
299+
300+
@override
301+
double? computeDistanceToActualBaseline(TextBaseline baseline) {
302+
return defaultComputeDistanceToFirstActualBaseline(baseline);
303+
}
304+
305+
// Next we have a function similar to the intrinsic methods, but for both axes
306+
// at the same time.
307+
308+
@override
309+
Size computeDryLayout(BoxConstraints constraints) {
310+
final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth);
311+
String? previousCategory;
312+
double y = 0.0;
313+
RenderBox? child = firstChild;
314+
while (child != null) {
315+
final String category = (child.parentData! as TextFlowParentData).category;
316+
if (previousCategory != null) {
317+
y += spacingBetween(previousCategory, category);
318+
}
319+
final Size childSize = child.getDryLayout(innerConstraints);
320+
y += childSize.height;
321+
previousCategory = category;
322+
child = childAfter(child);
323+
}
324+
return constraints.constrain(Size(constraints.maxWidth, y));
325+
}
326+
327+
// This is the core of the layout logic. Most of the time, this is the only
328+
// function that will be called. It computes the size and position of each
329+
// child, and stores it (in the parent data, as it happens!) for use during
330+
// the paint phase.
331+
332+
@override
333+
void performLayout() {
334+
final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth);
335+
String? previousCategory;
336+
double y = 0.0;
337+
RenderBox? child = firstChild;
338+
while (child != null) {
339+
final String category = (child.parentData! as TextFlowParentData).category;
340+
if (previousCategory != null) {
341+
// This is where we call the function that computes the spacing between
342+
// the different children. The arguments are the categories, obtained
343+
// from the parentData property of each child.
344+
y += spacingBetween(previousCategory, category);
345+
}
346+
child.layout(innerConstraints, parentUsesSize: true);
347+
(child.parentData! as TextFlowParentData).offset = Offset(0.0, y);
348+
y += child.size.height;
349+
previousCategory = category;
350+
child = childAfter(child);
351+
}
352+
size = constraints.constrain(Size(constraints.maxWidth, y));
353+
}
354+
355+
// Hit testing is normal for this widget, so we defer to the default implementation.
356+
@override
357+
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
358+
return defaultHitTestChildren(result, position: position);
359+
}
360+
361+
// Painting is normal for this widget, so we defer to the default
362+
// implementation. The default implementation expects to find the positions
363+
// configured in the parentData property of each child, which is why we
364+
// configure it that way in performLayout above.
365+
@override
366+
void paint(PaintingContext context, Offset offset) {
367+
defaultPaint(context, offset);
368+
}
369+
}
370+
371+
// Finally we have the two render objects that implement the two layouts in this demo.
372+
373+
class RenderOpenLayout extends RenderTextFlow {
374+
@override
375+
double spacingBetween(String before, String after) {
376+
if (after == 'headline') {
377+
return 20.0;
378+
}
379+
if (before == 'headline') {
380+
return 5.0;
381+
}
382+
return 10.0;
383+
}
384+
}
385+
386+
class RenderCompactLayout extends RenderTextFlow {
387+
@override
388+
double spacingBetween(String before, String after) {
389+
if (after == 'headline') {
390+
return 4.0;
391+
}
392+
return 2.0;
393+
}
394+
}

0 commit comments

Comments
 (0)