An example of parentData usage. (#131818)
This commit is contained in:
parent
e972d5a3f6
commit
f9a578dd82
394
examples/api/lib/rendering/box/parent_data.0.dart
Normal file
394
examples/api/lib/rendering/box/parent_data.0.dart
Normal file
@ -0,0 +1,394 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
void main() => runApp(const SampleApp());
|
||||
|
||||
class SampleApp extends StatefulWidget {
|
||||
const SampleApp({super.key});
|
||||
|
||||
@override
|
||||
State<SampleApp> createState() => _SampleAppState();
|
||||
}
|
||||
|
||||
class _SampleAppState extends State<SampleApp> {
|
||||
// This can be toggled using buttons in the UI to change which layout render object is used.
|
||||
bool _compact = false;
|
||||
|
||||
// This is the content we show in the rendering.
|
||||
//
|
||||
// Headline and Paragraph are simple custom widgets defined below.
|
||||
//
|
||||
// Any widget _could_ be specified here, and would render fine.
|
||||
// The Headline and Paragraph widgets are used so that the renderer
|
||||
// can distinguish between the kinds of content and use different
|
||||
// spacing between different children.
|
||||
static const List<Widget> body = <Widget>[
|
||||
Headline('Bugs that improve T for future bugs'),
|
||||
Paragraph(
|
||||
'The best bugs to fix are those that make us more productive '
|
||||
'in the future. Reducing test flakiness, reducing technical '
|
||||
'debt, increasing the number of team members who are able to '
|
||||
'review code confidently and well: this all makes future bugs '
|
||||
'easier to fix, which is a huge multiplier to our overall '
|
||||
'effectiveness and thus to developer happiness.',
|
||||
),
|
||||
Headline('Bugs affecting more people are more valuable (maximize N)'),
|
||||
Paragraph(
|
||||
'We will make more people happier if we fix a bug experienced by more people.'
|
||||
),
|
||||
Paragraph(
|
||||
'One thing to be careful about is to think about the number of '
|
||||
'people we are ignoring in our metrics. For example, if we had '
|
||||
'a bug that prevented our product from working on Windows, we '
|
||||
'would have no Windows users, so the bug would affect nobody. '
|
||||
'However, fixing the bug would enable millions of developers '
|
||||
"to use our product, and that's the number that counts."
|
||||
),
|
||||
Headline('Bugs with greater impact on developers are more valuable (maximize ΔH)'),
|
||||
Paragraph(
|
||||
'A slight improvement to the user experience is less valuable '
|
||||
'than a greater improvement. For example, if our application, '
|
||||
'under certain conditions, shows a message with a typo, and '
|
||||
'then crashes because of an off-by-one error in the code, '
|
||||
'fixing the crash is a higher priority than fixing the typo.'
|
||||
),
|
||||
];
|
||||
|
||||
// This is the description of the demo's interface.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Custom Render Boxes'),
|
||||
// There are two buttons over to the top right of the demo that let you
|
||||
// toggle between the two rendering modes.
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.density_small),
|
||||
isSelected: _compact,
|
||||
onPressed: () {
|
||||
setState(() { _compact = true; });
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.density_large),
|
||||
isSelected: !_compact,
|
||||
onPressed: () {
|
||||
setState(() { _compact = false; });
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 20.0),
|
||||
// CompactLayout and OpenLayout are the two rendering widgets defined below.
|
||||
child: _compact ? const CompactLayout(children: body) : const OpenLayout(children: body),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Headline and Paragraph are just wrappers around the Text widget, but they
|
||||
// also introduce a TextCategory widget that the CompactLayout and OpenLayout
|
||||
// widgets can read to determine what kind of child is being rendered.
|
||||
|
||||
class Headline extends StatelessWidget {
|
||||
const Headline(this.text, { super.key });
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextCategory(
|
||||
category: 'headline',
|
||||
child: Text(text, style: Theme.of(context).textTheme.titleLarge),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Paragraph extends StatelessWidget {
|
||||
const Paragraph(this.text, { super.key });
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextCategory(
|
||||
category: 'paragraph',
|
||||
child: Text(text, style: Theme.of(context).textTheme.bodyLarge),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// This is the ParentDataWidget that allows us to specify what kind of child
|
||||
// is being rendered. It allows information to be shared with the render object
|
||||
// without violating the principle of agnostic composition (wherein parents should
|
||||
// work with any child, not only support a fixed set of children).
|
||||
class TextCategory extends ParentDataWidget<TextFlowParentData> {
|
||||
const TextCategory({ super.key, required this.category, required super.child });
|
||||
|
||||
final String category;
|
||||
|
||||
@override
|
||||
void applyParentData(RenderObject renderObject) {
|
||||
final TextFlowParentData parentData = renderObject.parentData! as TextFlowParentData;
|
||||
if (parentData.category != category) {
|
||||
parentData.category = category;
|
||||
renderObject.parent!.markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Type get debugTypicalAncestorWidgetClass => OpenLayout;
|
||||
}
|
||||
|
||||
// This is one of the two layout variants. It is a widget that defers to
|
||||
// a render object defined below (RenderCompactLayout).
|
||||
class CompactLayout extends MultiChildRenderObjectWidget {
|
||||
const CompactLayout({ super.key, super.children });
|
||||
|
||||
@override
|
||||
RenderCompactLayout createRenderObject(BuildContext context) {
|
||||
return RenderCompactLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderCompactLayout renderObject) {
|
||||
// nothing to update
|
||||
}
|
||||
}
|
||||
|
||||
// This is the other of the two layout variants. It is a widget that defers to a
|
||||
// render object defined below (RenderOpenLayout).
|
||||
class OpenLayout extends MultiChildRenderObjectWidget {
|
||||
const OpenLayout({ super.key, super.children });
|
||||
|
||||
@override
|
||||
RenderOpenLayout createRenderObject(BuildContext context) {
|
||||
return RenderOpenLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderOpenLayout renderObject) {
|
||||
// nothing to update
|
||||
}
|
||||
}
|
||||
|
||||
// This is the data structure that contains the kind of data that can be
|
||||
// passed to the parent to label the child. It is literally stored on
|
||||
// the RenderObject child, in its "parentData" field.
|
||||
class TextFlowParentData extends ContainerBoxParentData<RenderBox> {
|
||||
String category = '';
|
||||
}
|
||||
|
||||
// This is the bulk of the layout logic. (It's similar to RenderListBody,
|
||||
// but only supports vertical layout.) It has no properties.
|
||||
//
|
||||
// This is an abstract class that is then extended by RenderCompactLayout and
|
||||
// RenderOpenLayout to get different layouts based on the children's categories,
|
||||
// as stored in the ParentData structure defined above.
|
||||
//
|
||||
// The documentation for the RenderBox class and its members provides much
|
||||
// more detail on how to implement each of the methods below.
|
||||
abstract class RenderTextFlow extends RenderBox
|
||||
with ContainerRenderObjectMixin<RenderBox, TextFlowParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, TextFlowParentData> {
|
||||
RenderTextFlow({ List<RenderBox>? children }) {
|
||||
addAll(children);
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! TextFlowParentData) {
|
||||
child.parentData = TextFlowParentData();
|
||||
}
|
||||
}
|
||||
|
||||
// This is the function that is overridden by the subclasses to do the
|
||||
// actual decision about the space to use between children.
|
||||
double spacingBetween(String before, String after);
|
||||
|
||||
// The next few functions are the layout functions. In each case we walk the
|
||||
// children, calling each one to determine the geometry of the child, and use
|
||||
// that to determine the layout.
|
||||
|
||||
// The first two functions compute the intrinsic width of the render object,
|
||||
// as seen when using the IntrinsicWidth widget.
|
||||
//
|
||||
// They essentially defer to the widest child.
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
double width = 0.0;
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final double childWidth = child.getMinIntrinsicWidth(height);
|
||||
if (childWidth > width) {
|
||||
width = childWidth;
|
||||
}
|
||||
child = childAfter(child);
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
double width = 0.0;
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final double childWidth = child.getMaxIntrinsicWidth(height);
|
||||
if (childWidth > width) {
|
||||
width = childWidth;
|
||||
}
|
||||
child = childAfter(child);
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
// The next two functions compute the intrinsic height of the render object,
|
||||
// as seen when using the IntrinsicHeight widget.
|
||||
//
|
||||
// They add up the height contributed by each child.
|
||||
//
|
||||
// They have to take into account the categories of the children and the
|
||||
// spacing that will be added, hence the slightly more elaborate logic.
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
String? previousCategory;
|
||||
double height = 0.0;
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final String category = (child.parentData! as TextFlowParentData).category;
|
||||
if (previousCategory != null) {
|
||||
height += spacingBetween(previousCategory, category);
|
||||
}
|
||||
height += child.getMinIntrinsicHeight(width);
|
||||
previousCategory = category;
|
||||
child = childAfter(child);
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
String? previousCategory;
|
||||
double height = 0.0;
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final String category = (child.parentData! as TextFlowParentData).category;
|
||||
if (previousCategory != null) {
|
||||
height += spacingBetween(previousCategory, category);
|
||||
}
|
||||
height += child.getMaxIntrinsicHeight(width);
|
||||
previousCategory = category;
|
||||
child = childAfter(child);
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
// This function implements the baseline logic. Because this class does
|
||||
// nothing special, we just defer to the default implementation in the
|
||||
// RenderBoxContainerDefaultsMixin utility class.
|
||||
|
||||
@override
|
||||
double? computeDistanceToActualBaseline(TextBaseline baseline) {
|
||||
return defaultComputeDistanceToFirstActualBaseline(baseline);
|
||||
}
|
||||
|
||||
// Next we have a function similar to the intrinsic methods, but for both axes
|
||||
// at the same time.
|
||||
|
||||
@override
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth);
|
||||
String? previousCategory;
|
||||
double y = 0.0;
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final String category = (child.parentData! as TextFlowParentData).category;
|
||||
if (previousCategory != null) {
|
||||
y += spacingBetween(previousCategory, category);
|
||||
}
|
||||
final Size childSize = child.getDryLayout(innerConstraints);
|
||||
y += childSize.height;
|
||||
previousCategory = category;
|
||||
child = childAfter(child);
|
||||
}
|
||||
return constraints.constrain(Size(constraints.maxWidth, y));
|
||||
}
|
||||
|
||||
// This is the core of the layout logic. Most of the time, this is the only
|
||||
// function that will be called. It computes the size and position of each
|
||||
// child, and stores it (in the parent data, as it happens!) for use during
|
||||
// the paint phase.
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
final BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth);
|
||||
String? previousCategory;
|
||||
double y = 0.0;
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final String category = (child.parentData! as TextFlowParentData).category;
|
||||
if (previousCategory != null) {
|
||||
// This is where we call the function that computes the spacing between
|
||||
// the different children. The arguments are the categories, obtained
|
||||
// from the parentData property of each child.
|
||||
y += spacingBetween(previousCategory, category);
|
||||
}
|
||||
child.layout(innerConstraints, parentUsesSize: true);
|
||||
(child.parentData! as TextFlowParentData).offset = Offset(0.0, y);
|
||||
y += child.size.height;
|
||||
previousCategory = category;
|
||||
child = childAfter(child);
|
||||
}
|
||||
size = constraints.constrain(Size(constraints.maxWidth, y));
|
||||
}
|
||||
|
||||
// Hit testing is normal for this widget, so we defer to the default implementation.
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
||||
return defaultHitTestChildren(result, position: position);
|
||||
}
|
||||
|
||||
// Painting is normal for this widget, so we defer to the default
|
||||
// implementation. The default implementation expects to find the positions
|
||||
// configured in the parentData property of each child, which is why we
|
||||
// configure it that way in performLayout above.
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
defaultPaint(context, offset);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally we have the two render objects that implement the two layouts in this demo.
|
||||
|
||||
class RenderOpenLayout extends RenderTextFlow {
|
||||
@override
|
||||
double spacingBetween(String before, String after) {
|
||||
if (after == 'headline') {
|
||||
return 20.0;
|
||||
}
|
||||
if (before == 'headline') {
|
||||
return 5.0;
|
||||
}
|
||||
return 10.0;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderCompactLayout extends RenderTextFlow {
|
||||
@override
|
||||
double spacingBetween(String before, String after) {
|
||||
if (after == 'headline') {
|
||||
return 4.0;
|
||||
}
|
||||
return 2.0;
|
||||
}
|
||||
}
|
17
examples/api/test/rendering/box/parent_data.0_test.dart
Normal file
17
examples/api/test/rendering/box/parent_data.0_test.dart
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_api_samples/rendering/box/parent_data.0.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('parent data example', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const SampleApp());
|
||||
expect(tester.getTopLeft(find.byType(Headline).at(2)), const Offset(30.0, 728.0));
|
||||
await tester.tap(find.byIcon(Icons.density_small));
|
||||
await tester.pump();
|
||||
expect(tester.getTopLeft(find.byType(Headline).at(2)), const Offset(30.0, 682.0));
|
||||
});
|
||||
}
|
@ -911,6 +911,15 @@ class BoxHitTestEntry extends HitTestEntry<RenderBox> {
|
||||
}
|
||||
|
||||
/// Parent data used by [RenderBox] and its subclasses.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// Parent data is used to communicate to a render object about its
|
||||
/// children. In this example, there are two render objects that perform
|
||||
/// text layout. They use parent data to identify the kind of child they
|
||||
/// are laying out, and space the children accordingly.
|
||||
///
|
||||
/// ** See code in examples/api/lib/rendering/box/parent_data.0.dart **
|
||||
/// {@end-tool}
|
||||
class BoxParentData extends ParentData {
|
||||
/// The offset at which to paint the child in the parent's coordinate system.
|
||||
Offset offset = Offset.zero;
|
||||
|
@ -1778,16 +1778,20 @@ abstract class InheritedWidget extends ProxyWidget {
|
||||
bool updateShouldNotify(covariant InheritedWidget oldWidget);
|
||||
}
|
||||
|
||||
/// RenderObjectWidgets provide the configuration for [RenderObjectElement]s,
|
||||
/// [RenderObjectWidget]s provide the configuration for [RenderObjectElement]s,
|
||||
/// which wrap [RenderObject]s, which provide the actual rendering of the
|
||||
/// application.
|
||||
///
|
||||
/// See also:
|
||||
/// Usually, rather than subclassing [RenderObjectWidget] directly, render
|
||||
/// object widgets subclass one of:
|
||||
///
|
||||
/// * [MultiChildRenderObjectWidget], which configures a [RenderObject] with
|
||||
/// a single list of children.
|
||||
/// * [SlottedMultiChildRenderObjectWidget], which configures a
|
||||
/// [RenderObject] that organizes its children in different named slots.
|
||||
/// * [LeafRenderObjectWidget], if the widget has no children.
|
||||
/// * [SingleChildRenderObjectElement], if the widget has exactly one child.
|
||||
/// * [MultiChildRenderObjectWidget], if the widget takes a list of children.
|
||||
/// * [SlottedMultiChildRenderObjectWidget], if the widget organizes its
|
||||
/// children in different named slots.
|
||||
///
|
||||
/// Subclasses must implement [createRenderObject] and [updateRenderObject].
|
||||
abstract class RenderObjectWidget extends Widget {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
@ -1830,8 +1834,10 @@ abstract class RenderObjectWidget extends Widget {
|
||||
void didUnmountRenderObject(covariant RenderObject renderObject) { }
|
||||
}
|
||||
|
||||
/// A superclass for RenderObjectWidgets that configure RenderObject subclasses
|
||||
/// A superclass for [RenderObjectWidget]s that configure [RenderObject] subclasses
|
||||
/// that have no children.
|
||||
///
|
||||
/// Subclasses must implement [createRenderObject] and [updateRenderObject].
|
||||
abstract class LeafRenderObjectWidget extends RenderObjectWidget {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
@ -1842,13 +1848,14 @@ abstract class LeafRenderObjectWidget extends RenderObjectWidget {
|
||||
}
|
||||
|
||||
/// A superclass for [RenderObjectWidget]s that configure [RenderObject] subclasses
|
||||
/// that have a single child slot. (This superclass only provides the storage
|
||||
/// for that child, it doesn't actually provide the updating logic.)
|
||||
/// that have a single child slot.
|
||||
///
|
||||
/// Typically, the render object assigned to this widget will make use of
|
||||
/// The render object assigned to this widget should make use of
|
||||
/// [RenderObjectWithChildMixin] to implement a single-child model. The mixin
|
||||
/// exposes a [RenderObjectWithChildMixin.child] property that allows
|
||||
/// retrieving the render object belonging to the [child] widget.
|
||||
/// exposes a [RenderObjectWithChildMixin.child] property that allows retrieving
|
||||
/// the render object belonging to the [child] widget.
|
||||
///
|
||||
/// Subclasses must implement [createRenderObject] and [updateRenderObject].
|
||||
abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
@ -1868,13 +1875,15 @@ abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
|
||||
/// storage for that child list, it doesn't actually provide the updating
|
||||
/// logic.)
|
||||
///
|
||||
/// Subclasses must return a [RenderObject] that mixes in
|
||||
/// Subclasses must use a [RenderObject] that mixes in
|
||||
/// [ContainerRenderObjectMixin], which provides the necessary functionality to
|
||||
/// visit the children of the container render object (the render object
|
||||
/// belonging to the [children] widgets). Typically, subclasses will return a
|
||||
/// belonging to the [children] widgets). Typically, subclasses will use a
|
||||
/// [RenderBox] that mixes in both [ContainerRenderObjectMixin] and
|
||||
/// [RenderBoxContainerDefaultsMixin].
|
||||
///
|
||||
/// Subclasses must implement [createRenderObject] and [updateRenderObject].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Stack], which uses [MultiChildRenderObjectWidget].
|
||||
@ -6538,8 +6547,8 @@ class LeafRenderObjectElement extends RenderObjectElement {
|
||||
///
|
||||
/// The child is optional.
|
||||
///
|
||||
/// This element subclass can be used for RenderObjectWidgets whose
|
||||
/// RenderObjects use the [RenderObjectWithChildMixin] mixin. Such widgets are
|
||||
/// This element subclass can be used for [RenderObjectWidget]s whose
|
||||
/// [RenderObject]s use the [RenderObjectWithChildMixin] mixin. Such widgets are
|
||||
/// expected to inherit from [SingleChildRenderObjectWidget].
|
||||
class SingleChildRenderObjectElement extends RenderObjectElement {
|
||||
/// Creates an element that uses the given widget as its configuration.
|
||||
@ -6600,8 +6609,8 @@ class SingleChildRenderObjectElement extends RenderObjectElement {
|
||||
|
||||
/// An [Element] that uses a [MultiChildRenderObjectWidget] as its configuration.
|
||||
///
|
||||
/// This element subclass can be used for RenderObjectWidgets whose
|
||||
/// RenderObjects use the [ContainerRenderObjectMixin] mixin with a parent data
|
||||
/// This element subclass can be used for [RenderObjectWidget]s whose
|
||||
/// [RenderObject]s use the [ContainerRenderObjectMixin] mixin with a parent data
|
||||
/// type that implements [ContainerParentDataMixin<RenderObject>]. Such widgets
|
||||
/// are expected to inherit from [MultiChildRenderObjectWidget].
|
||||
///
|
||||
|
Loading…
x
Reference in New Issue
Block a user