blob: 0063f0e54427d17d464629593a85aaeb7ec9fd0d [file] [log] [blame]
// 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;
}
}