| // 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; |
| } |
| } |