Pre-conditions:
- We can use a
LayoutBuilder
to obtain the height available forListView
- We don't know the height of Widget A or Widget B.
How can Widget B be centered in the available space in the left situation while still not causing Flutter to use infinite height for the space to center Widget B within?
Some basic code for the situation:
LayoutBuilder(builder: (context, constraints) {
return ListView(
children: [
Container(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Column(
children: [
WidgetA(),
WidgetB(),
],
),
),
]
);
}),
I would like to wrap WidgetB in Expanded but only up to the total height of constraints.maxHeight
for WidgetA WidgetB to not get the infinite height exception.
LayoutBuilder(builder: (context, constraints) {
return ListView(
children: [
Container(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Column(
children: [
WidgetA(),
Expanded(WidgetB()), // Infinite height exception
],
),
),
]
);
}),
CodePudding user response:
Constraints go down. Size go up. This solution replaces Column
with a custom RenderBox
which is able to layout the children with width constraints from the parent and uses children height and passed in available screen height to layout widgetB appropriately.
Last in performLayout()
it computes the height of the container and passes up based on children height and screenHeight
.
Usage example:
CustomColumnLayout(
widgetA: WidgetA(),
widgetB: WidgetB(),
)
Solution:
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
class CustomColumnLayout extends StatelessWidget {
const CustomColumnLayout({
Key? key,
required this.widgetA,
required this.widgetB,
}) : super(key: key);
final Widget widgetA;
final Widget widgetB;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: ((context, constraints) {
return ListView(children: [
CustomLayout(
widgetA: widgetA,
widgetB: widgetB,
screenHeight: constraints.maxHeight,
)
]);
}),
);
}
}
enum CustomSlot {
widgetA,
widgetB,
}
class CustomLayout extends RenderObjectWidget
with SlottedMultiChildRenderObjectWidgetMixin<CustomSlot> {
const CustomLayout({
Key? key,
required this.widgetA,
required this.widgetB,
required this.screenHeight,
}) : super(key: key);
final Widget widgetA;
final Widget widgetB;
final double screenHeight;
@override
Iterable<CustomSlot> get slots => CustomSlot.values;
@override
Widget? childForSlot(CustomSlot slot) {
switch (slot) {
case CustomSlot.widgetA:
return widgetA;
case CustomSlot.widgetB:
return widgetB;
}
}
@override
SlottedContainerRenderObjectMixin<CustomSlot> createRenderObject(
BuildContext context,
) {
return RenderCustomLayout(screenHeight: screenHeight);
}
@override
void updateRenderObject(
BuildContext context,
SlottedContainerRenderObjectMixin<CustomSlot> renderObject,
) {
(renderObject as RenderCustomLayout).screenHeight = screenHeight;
}
}
/// A render object that demonstrates the usage of [SlottedContainerRenderObjectMixin]
/// by providing slots for two children that will be arranged diagonally.
class RenderCustomLayout extends RenderBox
with
SlottedContainerRenderObjectMixin<CustomSlot>,
DebugOverflowIndicatorMixin {
RenderCustomLayout({required double screenHeight})
: _screenHeight = screenHeight;
@override
bool get sizedByParent => false;
// Getters and setters to configure the [RenderObject] with the configuration
// of the [Widget]. These mostly contain boilerplate code, but depending on
// where the configuration value is used, the setter has to call
// [markNeedsLayout], [markNeedsPaint], or [markNeedsSemanticsUpdate].
double get screenHeight => _screenHeight;
double _screenHeight;
set screenHeight(double value) {
if (_screenHeight == value) {
return;
}
_screenHeight = value;
markNeedsLayout();
}
// Getters to simplify accessing the slotted children.
RenderBox? get _widgetA => childForSlot(CustomSlot.widgetA);
RenderBox? get _widgetB => childForSlot(CustomSlot.widgetB);
// The size this render object would have if the incoming constraints were
// unconstrained; calculated during performLayout used during paint for an
// assertion that checks for unintended overflow.
late Size _childrenSize;
// Returns children in hit test order.
@override
Iterable<RenderBox> get children {
return <RenderBox>[
if (_widgetA != null) _widgetA!,
if (_widgetB != null) _widgetB!,
];
}
// LAYOUT
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
final BoxConstraints childConstraints = BoxConstraints(
minWidth: this.constraints.minWidth,
maxWidth: this.constraints.minWidth,
);
// Lay out the top left child and position it at offset zero.
Size widgetASize = Size.zero;
final RenderBox? widgetA = _widgetA;
if (widgetA != null) {
widgetA.layout(childConstraints, parentUsesSize: true);
_positionChild(widgetA, Offset.zero);
widgetASize = widgetA.size;
}
// Lay out the bottom right child and position it at the bottom right corner
// of the top left child.
Size widgetBSize = Size.zero;
final RenderBox? widgetB = _widgetB;
if (widgetB != null) {
widgetB.layout(childConstraints, parentUsesSize: true);
widgetBSize = widgetB.size;
final widgetBY = widgetASize.height
max(0, (screenHeight - widgetASize.height - widgetBSize.height) / 2);
_positionChild(
widgetB,
Offset(0, widgetBY),
);
}
// Calculate the overall size and constrain it to the given constraints.
// Any overflow is marked (in debug mode) during paint.
_childrenSize = Size(
max(widgetASize.width, widgetBSize.width),
max(widgetASize.height widgetBSize.height, screenHeight),
);
size = constraints.constrain(_childrenSize);
}
void _positionChild(RenderBox child, Offset offset) {
(child.parentData! as BoxParentData).offset = offset;
}
// PAINT
@override
void paint(PaintingContext context, Offset offset) {
void paintChild(RenderBox child, PaintingContext context, Offset offset) {
final BoxParentData childParentData = child.parentData! as BoxParentData;
context.paintChild(child, childParentData.offset offset);
}
// Paint the children at the offset calculated during layout.
final RenderBox? widgetA = _widgetA;
if (widgetA != null) {
paintChild(widgetA, context, offset);
}
final RenderBox? widgetB = _widgetB;
if (widgetB != null) {
paintChild(widgetB, context, offset);
}
// Paint an overflow indicator in debug mode if the children want to be
// larger than the incoming constraints allow.
assert(() {
paintOverflowIndicator(
context,
offset,
Offset.zero & size,
Offset.zero & _childrenSize,
);
return true;
}());
}
// HIT TEST
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
for (final RenderBox child in children) {
final BoxParentData parentData = child.parentData! as BoxParentData;
final bool isHit = result.addWithPaintOffset(
offset: parentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - parentData.offset);
return child.hitTest(result, position: transformed);
},
);
if (isHit) {
return true;
}
}
return false;
}
// INTRINSICS
// Incoming height/width are ignored as children are always laid out unconstrained.
@override
double computeMinIntrinsicWidth(double height) {
final double widgetAWidth =
_widgetA?.getMinIntrinsicWidth(double.infinity) ?? 0;
final double widgetBWidth =
_widgetB?.getMinIntrinsicWidth(double.infinity) ?? 0;
return max(widgetAWidth, widgetBWidth);
}
@override
double computeMaxIntrinsicWidth(double height) {
final double widgetAWidth =
_widgetA?.getMinIntrinsicWidth(double.infinity) ?? 0;
final double widgetBWidth =
_widgetB?.getMinIntrinsicWidth(double.infinity) ?? 0;
return max(widgetAWidth, widgetBWidth);
}
@override
double computeMinIntrinsicHeight(double width) {
final double widgetAHeight =
_widgetA?.getMinIntrinsicHeight(double.infinity) ?? 0;
final double widgetBHeight =
_widgetB?.getMinIntrinsicHeight(double.infinity) ?? 0;
return max(widgetAHeight widgetBHeight, screenHeight);
}
@override
double computeMaxIntrinsicHeight(double width) {
final double widgetAHeight =
_widgetA?.getMinIntrinsicHeight(double.infinity) ?? 0;
final double widgetBHeight =
_widgetB?.getMinIntrinsicHeight(double.infinity) ?? 0;
return max(widgetAHeight widgetBHeight, screenHeight);
}
@override
Size computeDryLayout(BoxConstraints constraints) {
assert(debugCannotComputeDryLayout(
reason: 'Dry layout is not supported',
));
return Size.zero;
}
}
At first I thought I could subclass CustomMultiChildLayout
and implement a MultiChildLayoutDelegate
subclass that layouts WidgetA and WidgetB, get their sizes and can position the widgets. However, CustomMultiChildLayout
is not possible to use because it cannot affect the size of the container. To do that one need to dig a step deeper into render objects which I've done above.
To find the final solution I used SlottedMultiChildRenderObjectWidgetMixin documentation and RenderBox source code.
CodePudding user response:
try this code:
LayoutBuilder(builder: (context, constraints) {
return ListView(
children: [
Column(
children: [
//WidgetA
Container(
height: MediaQuery.of(context).size.height * 0.3,
width: MediaQuery.of(context).size.width,
child: WidgetA(),
),
//WidgetB
Container(
height: MediaQuery.of(context).size.height * 0.7,
width: MediaQuery.of(context).size.width,
child: WidgetB(),
),
],
),
]
);
}),