Home > Mobile >  How to center in available space, but not use infinite height in Flutter?
How to center in available space, but not use infinite height in Flutter?

Time:02-14

Pre-conditions:

  • We can use a LayoutBuilder to obtain the height available for ListView
  • 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?

enter image description here

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(),
          ),
        ],
      ),
    ]
  );
}),

  • Related