Skip to content

Instantly share code, notes, and snippets.

@jxw1102
Last active February 12, 2024 07:21
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save jxw1102/a9b58a78a80c8e2f54233b418429fa50 to your computer and use it in GitHub Desktop.
Save jxw1102/a9b58a78a80c8e2f54233b418429fa50 to your computer and use it in GitHub Desktop.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:math' as math;
class CustomLayout extends MultiChildRenderObjectWidget {
CustomLayout({
Key key,
List<Widget> children = const <Widget>[],
}) : super(key: key, children: children);
@override
RenderCustomLayoutBox createRenderObject(BuildContext context) {
return RenderCustomLayoutBox();
}
}
class RenderCustomLayoutBox extends RenderBox
with ContainerRenderObjectMixin<RenderBox, CustomLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, CustomLayoutParentData> {
RenderCustomLayoutBox({
List<RenderBox> children,
}) {
addAll(children);
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! CustomLayoutParentData) {
child.parentData = CustomLayoutParentData();
}
}
double _getIntrinsicHeight(double childSize(RenderBox child)) {
double inflexibleSpace = 0.0;
RenderBox child = firstChild;
while (child != null) {
if (child == lastChild)
break;
inflexibleSpace += childSize(child);
final FlexParentData childParentData = child.parentData;
child = childParentData.nextSibling;
}
return inflexibleSpace;
}
double _getIntrinsicWidth(double childSize(RenderBox child)) {
double maxSpace = 0.0;
RenderBox child = firstChild;
while (child != null) {
if (child == lastChild)
break;
maxSpace = math.max(maxSpace, childSize(child));
final FlexParentData childParentData = child.parentData;
child = childParentData.nextSibling;
}
return maxSpace;
}
@override
double computeMinIntrinsicWidth(double height) {
return _getIntrinsicWidth((RenderBox child) => child.getMinIntrinsicWidth(height));
}
@override
double computeMaxIntrinsicWidth(double height) {
return _getIntrinsicWidth((RenderBox child) => child.getMaxIntrinsicWidth(height));
}
@override
double computeMinIntrinsicHeight(double width) {
return _getIntrinsicHeight((RenderBox child) => child.getMinIntrinsicHeight(width));
}
@override
double computeMaxIntrinsicHeight(double width) {
return _getIntrinsicHeight((RenderBox child) => child.getMaxIntrinsicHeight(width));
}
@override
void performLayout() {
if (childCount == 0) {
size = constraints.biggest;
assert(size.isFinite);
return;
}
double width = constraints.maxWidth;
double height = 0;
RenderBox child = firstChild;
while (child != null) {
if (child == lastChild)
break;
final CustomLayoutParentData childParentData = child.parentData;
child.layout(BoxConstraints.tightFor(width: width), parentUsesSize: true);
childParentData.offset = Offset(0, height);
final Size childSize = child.size;
width = math.max(width, childSize.width);
height += childSize.height;
child = childParentData.nextSibling;
}
size = Size(width, height);
lastChild.layout(BoxConstraints(maxWidth: width, maxHeight: height), parentUsesSize: true);
final CustomLayoutParentData childParentData = lastChild.parentData;
final double margin = 20;
final double x = size.width - lastChild.size.width - margin;
final double y = height - childParentData.previousSibling.size.height - lastChild.size.height / 2;
childParentData.offset = Offset(x, y);
}
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
@override
bool hitTestChildren(HitTestResult result, { Offset position }) {
return defaultHitTestChildren(result, position: position);
}
}
class CustomLayoutParentData extends ContainerBoxParentData<RenderBox> {
}
@jxw1102
Copy link
Author

jxw1102 commented Jan 7, 2019

This custom layout will put all children in a column, except the last one. The last child's vertical center will be aligned to the top of its previous sibling.

screen shot 2019-01-08 at 00 54 33

@estevez-dev
Copy link

That's what I was looking for a long time! Thanks a lot for the example. I build a dynamic multi-column layout based on your example and it works just fine!

@oravecz
Copy link

oravecz commented Sep 14, 2020

Thanks for this pattern. Any advice for someone who would like to enforce the meaning of the children rather than depend on position and such? Think of your example, but I would like to pass the children in using properties like top, bottom, overlay. If I do this, is it important to call addAll([top, bottom, overlay]) or maintain a children list? I wouldn't necessarily use firstChild to return a RenderBox, but I don't know how to layout if I have a reference to the Widget, and not the RenderBox.

Or would you recommend, I pass in the widgets as children, and then interrogate the RenderBox during performLayout to determine which widget it represents?

@gmaggio
Copy link

gmaggio commented Jan 13, 2024

Here's my update for a nullable version:

class CustomLayout extends MultiChildRenderObjectWidget {
  const CustomLayout({
    super.key,
    super.children = const <Widget>[],
  });

  @override
  RenderCustomLayoutBox createRenderObject(BuildContext context) {
    return RenderCustomLayoutBox();
  }
}

class RenderCustomLayoutBox extends RenderBox
    with
        ContainerRenderObjectMixin<RenderBox, CustomLayoutParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox, CustomLayoutParentData> {
  RenderCustomLayoutBox({
    List<RenderBox>? children,
  }) {
    addAll(children);
  }

  @override
  void setupParentData(RenderBox child) {
    if (child.parentData is! CustomLayoutParentData) {
      child.parentData = CustomLayoutParentData();
    }
  }

  double _getIntrinsicHeight(double Function(RenderBox child) childSize) {
    double inflexibleSpace = 0.0;
    RenderBox? child = firstChild;
    while (child != null) {
      if (child == lastChild) break;
      inflexibleSpace += childSize(child);
      final FlexParentData childParentData = child.parentData as FlexParentData;
      child = childParentData.nextSibling;
    }
    return inflexibleSpace;
  }

  double _getIntrinsicWidth(double Function(RenderBox child) childSize) {
    double maxSpace = 0.0;
    RenderBox? child = firstChild;
    while (child != null) {
      if (child == lastChild) break;
      maxSpace = math.max(maxSpace, childSize(child));
      final FlexParentData childParentData = child.parentData as FlexParentData;
      child = childParentData.nextSibling;
    }
    return maxSpace;
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    return _getIntrinsicWidth(
      (RenderBox child) => child.getMinIntrinsicWidth(height),
    );
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    return _getIntrinsicWidth(
      (RenderBox child) => child.getMaxIntrinsicWidth(height),
    );
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    return _getIntrinsicHeight(
      (RenderBox child) => child.getMinIntrinsicHeight(width),
    );
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    return _getIntrinsicHeight(
      (RenderBox child) => child.getMaxIntrinsicHeight(width),
    );
  }

  @override
  void performLayout() {
    if (childCount == 0) {
      size = constraints.biggest;
      assert(size.isFinite);
      return;
    }

    double width = constraints.maxWidth;
    double height = 0;

    RenderBox? child = firstChild;
    while (child != null) {
      if (child == lastChild) break;

      final CustomLayoutParentData childParentData =
          child.parentData as CustomLayoutParentData;

      child.layout(BoxConstraints.tightFor(width: width), parentUsesSize: true);
      childParentData.offset = Offset(0, height);

      final Size childSize = child.size;
      width = math.max(width, childSize.width);
      height += childSize.height;

      child = childParentData.nextSibling;
    }

    size = Size(width, height);

    lastChild?.layout(
      BoxConstraints(maxWidth: width, maxHeight: height),
      parentUsesSize: true,
    );

    final CustomLayoutParentData? childParentData =
        lastChild?.parentData as CustomLayoutParentData?;

    const double margin = 20;

    final double x = size.width - (lastChild?.size.width ?? 0) - margin;
    final double y = height -
        (childParentData?.previousSibling?.size.height ?? 0) -
        (lastChild?.size.height ?? 0) / 2;
    childParentData?.offset = Offset(x, y);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }

  @override
  bool hitTestChildren(HitTestResult result, {required Offset position}) {
    return defaultHitTestChildren(
      result as BoxHitTestResult,
      position: position,
    );
  }
}

class CustomLayoutParentData extends ContainerBoxParentData<RenderBox> {}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment