Skip to content

Instantly share code, notes, and snippets.

@MaximilianKlein
Created August 7, 2020 18:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MaximilianKlein/9c9576f1840823cac44b2ce63fd200e3 to your computer and use it in GitHub Desktop.
Save MaximilianKlein/9c9576f1840823cac44b2ce63fd200e3 to your computer and use it in GitHub Desktop.
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ExampleCard(
title: 'Break due to long text',
btn1Text: 'say hello to everyone',
btn2Text: 'say bye to everyone',
),
ExampleCard(
title: 'One-Line Layout',
btn1Text: 'hey',
btn2Text: 'bye',
),
])
),
),
);
}
}
class ExampleCard extends StatelessWidget {
const ExampleCard({
@required this.title,
@required this.btn1Text,
@required this.btn2Text,
});
final String title;
final String btn1Text;
final String btn2Text;
Widget build(BuildContext context) =>
Card(child:
Container(
padding: EdgeInsets.all(10),
width: 300,
child: Column(children:[
Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Text(title, style: DefaultTextStyle.of(context).style.apply(fontSizeFactor: 2.0)),
),
ResponsiveWrap(
first: RaisedButton(
child: Text(btn1Text),
onPressed: () {}),
second: RaisedButton(
child: Text(btn2Text),
onPressed: () {}),
spacing: 10),
])));
}
/// These widgets are "similar" to those found in flutter rendering.
/// The only difference to CustomMultiChildLayout is, that these classes
/// allow to "relayout children" (which is normally allowed in flutter, but
/// it is disabled for CustomMultiChildLayout). You should NOT use children
/// that are complex to layout as it might create a lot of extra work.
/// Other than that you can use it exactly like the CustomMultiChildLayout
/// use MultiLayoutId instead of LayoutID and MultiChildMultiLayoutDelegate
/// instead of MultiChildLayoutDelegate.
class CustomMultiChildMultiLayout extends MultiChildRenderObjectWidget {
/// Creates a custom multi-child layout.
///
/// The [delegate] argument must not be null.
CustomMultiChildMultiLayout({
Key key,
@required this.delegate,
List<Widget> children = const <Widget>[],
}) : assert(delegate != null),
super(key: key, children: children);
/// The delegate that controls the layout of the children.
final MultiChildMultiLayoutDelegate delegate;
@override
RenderCustomMultiChildMultiLayoutBox createRenderObject(BuildContext context) {
return RenderCustomMultiChildMultiLayoutBox(delegate: delegate);
}
@override
void updateRenderObject(
BuildContext context, RenderCustomMultiChildMultiLayoutBox renderObject) {
renderObject.delegate = delegate;
}
}
class RenderCustomMultiChildMultiLayoutBox extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
/// Creates a render object that customizes the layout of multiple children.
///
/// The [delegate] argument must not be null.
RenderCustomMultiChildMultiLayoutBox({
@required MultiChildMultiLayoutDelegate delegate,
}) : assert(delegate != null),
_delegate = delegate;
@override
void setupParentData(RenderBox child) {
if (child.parentData is! MultiChildLayoutParentData)
child.parentData = MultiChildLayoutParentData();
}
/// The delegate that controls the layout of the children.
MultiChildMultiLayoutDelegate get delegate => _delegate;
MultiChildMultiLayoutDelegate _delegate;
set delegate(MultiChildMultiLayoutDelegate newDelegate) {
assert(newDelegate != null);
if (_delegate == newDelegate) return;
final MultiChildMultiLayoutDelegate oldDelegate = _delegate;
if (newDelegate.runtimeType != oldDelegate.runtimeType ||
newDelegate.shouldRelayout(oldDelegate)) markNeedsLayout();
_delegate = newDelegate;
if (attached) {
oldDelegate?._relayout?.removeListener(markNeedsLayout);
newDelegate?._relayout?.addListener(markNeedsLayout);
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_delegate?._relayout?.addListener(markNeedsLayout);
}
@override
void detach() {
_delegate?._relayout?.removeListener(markNeedsLayout);
super.detach();
}
@override
void performLayout() {
size = delegate._callPerformLayout(constraints, firstChild);
}
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
@override
bool hitTestChildren(BoxHitTestResult result, {Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}
class MultiLayoutId extends ParentDataWidget<MultiChildLayoutParentData> {
/// Marks a child with a layout identifier.
///
/// Both the child and the id arguments must not be null.
MultiLayoutId({
Key key,
@required this.id,
@required Widget child,
}) : assert(child != null),
assert(id != null),
super(key: key ?? ValueKey<Object>(id), child: child);
/// An object representing the identity of this child.
///
/// The [id] needs to be unique among the children that the
/// [CustomMultiChildLayout] manages.
final Object id;
@override
Type get debugTypicalAncestorWidgetClass => CustomMultiChildLayout;
@override
void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is MultiChildLayoutParentData);
final MultiChildLayoutParentData parentData = renderObject.parentData;
if (parentData.id != id) {
parentData.id = id;
final AbstractNode targetParent = renderObject.parent;
if (targetParent is RenderObject) targetParent.markNeedsLayout();
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Object>('id', id));
}
}
abstract class MultiChildMultiLayoutDelegate {
/// Creates a layout delegate.
///
/// The layout will update whenever [relayout] notifies its listeners.
MultiChildMultiLayoutDelegate({Listenable relayout}) : _relayout = relayout;
final Listenable _relayout;
Map<Object, RenderBox> _idToChild;
Set<RenderBox> _debugChildrenNeedingLayout;
/// True if a non-null LayoutChild was provided for the specified id.
///
/// Call this from the [performLayout] or [getSize] methods to
/// determine which children are available, if the child list might
/// vary.
bool hasChild(Object childId) => _idToChild[childId] != null;
/// Ask the child to update its layout within the limits specified by
/// the constraints parameter. The child's size is returned.
///
/// Call this from your [performLayout] function to lay out each
/// child. Every child must be laid out using this function exactly
/// once each time the [performLayout] function is called.
Size layoutChild(Object childId, BoxConstraints constraints) {
final RenderBox child = _idToChild[childId];
assert(() {
if (child == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'The $this custom multichild layout delegate tried to lay out a non-existent child.'),
ErrorDescription('There is no child with the id "$childId".')
]);
}
_debugChildrenNeedingLayout.remove(child);
try {
assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
} on AssertionError catch (exception) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".'),
DiagnosticsProperty<AssertionError>('Exception', exception,
showName: false),
ErrorDescription(
'The minimum width and height must be greater than or equal to zero.\n'
'The maximum width must be greater than or equal to the minimum width.\n'
'The maximum height must be greater than or equal to the minimum height.')
]);
}
return true;
}());
child.layout(constraints, parentUsesSize: true);
return child.size;
}
/// Specify the child's origin relative to this origin.
///
/// Call this from your [performLayout] function to position each
/// child. If you do not call this for a child, its position will
/// remain unchanged. Children initially have their position set to
/// (0,0), i.e. the top left of the [RenderCustomMultiChildLayoutBox].
void positionChild(Object childId, Offset offset) {
final RenderBox child = _idToChild[childId];
assert(() {
if (child == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'The $this custom multichild layout delegate tried to position out a non-existent child:'),
ErrorDescription('There is no child with the id "$childId".')
]);
}
if (offset == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'The $this custom multichild layout delegate provided a null position for the child with id "$childId".')
]);
}
return true;
}());
final MultiChildLayoutParentData childParentData = child.parentData;
childParentData.offset = offset;
}
DiagnosticsNode _debugDescribeChild(RenderBox child) {
final MultiChildLayoutParentData childParentData = child.parentData;
return DiagnosticsProperty<RenderBox>('${childParentData.id}', child);
}
Size _callPerformLayout(BoxConstraints constraints, RenderBox firstChild) {
// A particular layout delegate could be called reentrantly, e.g. if it used
// by both a parent and a child. So, we must restore the _idToChild map when
// we return.
final Map<Object, RenderBox> previousIdToChild = _idToChild;
Set<RenderBox> debugPreviousChildrenNeedingLayout;
assert(() {
debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
_debugChildrenNeedingLayout = <RenderBox>{};
return true;
}());
try {
_idToChild = <Object, RenderBox>{};
RenderBox child = firstChild;
while (child != null) {
final MultiChildLayoutParentData childParentData = child.parentData;
assert(() {
if (childParentData.id == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.'),
child.describeForError('The following child has no ID'),
]);
}
return true;
}());
_idToChild[childParentData.id] = child;
assert(() {
_debugChildrenNeedingLayout.add(child);
return true;
}());
child = childParentData.nextSibling;
}
Size size = performLayout(constraints);
assert(() {
if (_debugChildrenNeedingLayout.isNotEmpty) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Each child must be laid out exactly once.'),
DiagnosticsBlock(
name: 'The $this custom multichild layout delegate forgot '
'to lay out the following '
'${_debugChildrenNeedingLayout.length > 1 ? 'children' : 'child'}',
properties: _debugChildrenNeedingLayout
.map<DiagnosticsNode>(_debugDescribeChild)
.toList(),
style: DiagnosticsTreeStyle.whitespace,
),
]);
}
return true;
}());
return size;
} finally {
_idToChild = previousIdToChild;
assert(() {
_debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
return true;
}());
}
}
/// Override this method to lay out and position all children given this
/// widget's size.
///
/// This method must call [layoutChild] for each child. It should also specify
/// the final position of each child with [positionChild].
Size performLayout(BoxConstraints constraints);
/// Override this method to return true when the children need to be
/// laid out.
///
/// This should compare the fields of the current delegate and the given
/// `oldDelegate` and return true if the fields are such that the layout would
/// be different.
bool shouldRelayout(covariant MultiChildMultiLayoutDelegate oldDelegate);
/// Override this method to include additional information in the
/// debugging data printed by [debugDumpRenderTree] and friends.
///
/// By default, returns the [runtimeType] of the class.
@override
String toString() => '$runtimeType';
}
class ResponsiveWrap extends StatelessWidget {
///
ResponsiveWrap(
{@required this.spacing, @required this.first, @required this.second});
final double spacing;
final Widget first;
final Widget second;
@override
Widget build(BuildContext context) {
return CustomMultiChildMultiLayout(
children: <Widget>[
MultiLayoutId(
id: 1,
child: first,
),
MultiLayoutId(
id: 2,
child: second,
),
],
delegate: _RenderWrap(spacing: spacing),
);
}
}
class _RenderWrap extends MultiChildMultiLayoutDelegate {
_RenderWrap({this.spacing});
final double spacing;
@override
Size performLayout(BoxConstraints constraints) {
Size size;
final innerConstraints = constraints.copyWith(minWidth: 0, minHeight: 0);
// size = _constraints.biggest;
final firstChildSize = layoutChild(1, innerConstraints);
final secondChildSize = layoutChild(2, innerConstraints);
if (firstChildSize.width + spacing * 0.5 <= constraints.maxWidth * 0.5 &&
secondChildSize.width + spacing * 0.5 <= constraints.maxWidth * 0.5) {
final buttonSize = (constraints.maxWidth / 2) - (spacing / 2);
size = constraints.constrain(Size(constraints.maxWidth,
max(firstChildSize.height, secondChildSize.height)));
layoutChild(
1,
innerConstraints.copyWith(
minWidth: buttonSize, maxWidth: buttonSize));
layoutChild(
2,
innerConstraints.copyWith(
minWidth: buttonSize, maxWidth: buttonSize));
positionChild(2, Offset(buttonSize + spacing, 0));
} else {
final firstChildSize = layoutChild(
1, innerConstraints.copyWith(minWidth: constraints.maxWidth));
final secondChildSize = layoutChild(
2, innerConstraints.copyWith(minWidth: constraints.maxWidth));
final totalHeight =
firstChildSize.height + secondChildSize.height + spacing;
size = constraints.constrain(Size(constraints.maxWidth, totalHeight));
positionChild(2, Offset(0, firstChildSize.height + spacing));
}
return size;
}
@override
bool shouldRelayout(MultiChildMultiLayoutDelegate oldDelegate) {
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment