| // 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 'dart:math' as math; |
| |
| import 'package:flutter/rendering.dart'; |
| |
| import 'basic.dart'; |
| import 'framework.dart'; |
| |
| /// Defines the horizontal alignment of [OverflowBar] children |
| /// when they're laid out in an overflow column. |
| /// |
| /// This value must be interpreted relative to the ambient |
| /// [TextDirection]. |
| enum OverflowBarAlignment { |
| /// Each child is left-aligned for [TextDirection.ltr], |
| /// right-aligned for [TextDirection.rtl]. |
| start, |
| |
| /// Each child is right-aligned for [TextDirection.ltr], |
| /// left-aligned for [TextDirection.rtl]. |
| end, |
| |
| /// Each child is horizontally centered. |
| center, |
| } |
| |
| /// A widget that lays out its [children] in a row unless they |
| /// "overflow" the available horizontal space, in which case it lays |
| /// them out in a column instead. |
| /// |
| /// This widget's width will expand to contain its children and the |
| /// specified [spacing] until it overflows. The overflow column will |
| /// consume all of the available width. The [overflowAlignment] |
| /// defines how each child will be aligned within the overflow column |
| /// and the [overflowSpacing] defines the gap between each child. |
| /// |
| /// The order that the children appear in the horizontal layout |
| /// is defined by the [textDirection], just like the [Row] widget. |
| /// If the layout overflows, then children's order within their |
| /// column is specified by [overflowDirection] instead. |
| /// |
| /// {@tool dartpad} |
| /// This example defines a simple approximation of a dialog |
| /// layout, where the layout of the dialog's action buttons are |
| /// defined by an [OverflowBar]. The content is wrapped in a |
| /// [SingleChildScrollView], so that if overflow occurs, the |
| /// action buttons will still be accessible by scrolling, |
| /// no matter how much vertical space is available. |
| /// |
| /// ** See code in examples/api/lib/widgets/overflow_bar/overflow_bar.0.dart ** |
| /// {@end-tool} |
| class OverflowBar extends MultiChildRenderObjectWidget { |
| /// Constructs an OverflowBar. |
| /// |
| /// The [spacing], [overflowSpacing], [overflowAlignment], |
| /// [overflowDirection], and [clipBehavior] parameters must not be |
| /// null. The [children] argument must not be null and must not contain |
| /// any null objects. |
| OverflowBar({ |
| super.key, |
| this.spacing = 0.0, |
| this.alignment, |
| this.overflowSpacing = 0.0, |
| this.overflowAlignment = OverflowBarAlignment.start, |
| this.overflowDirection = VerticalDirection.down, |
| this.textDirection, |
| this.clipBehavior = Clip.none, |
| super.children, |
| }) : assert(spacing != null), |
| assert(overflowSpacing != null), |
| assert(overflowAlignment != null), |
| assert(overflowDirection != null), |
| assert(clipBehavior != null); |
| |
| /// The width of the gap between [children] for the default |
| /// horizontal layout. |
| /// |
| /// If the horizontal layout overflows, then [overflowSpacing] is |
| /// used instead. |
| /// |
| /// Defaults to 0.0. |
| final double spacing; |
| |
| /// Defines the [children]'s horizontal layout according to the same |
| /// rules as for [Row.mainAxisAlignment]. |
| /// |
| /// If this property is non-null, and the [children], separated by |
| /// [spacing], fit within the available width, then the overflow |
| /// bar will be as wide as possible. If the children do not fit |
| /// within the available width, then this property is ignored and |
| /// [overflowAlignment] applies instead. |
| /// |
| /// If this property is null (the default) then the overflow bar |
| /// will be no wider than needed to layout the [children] separated |
| /// by [spacing], modulo the incoming constraints. |
| /// |
| /// If [alignment] is one of [MainAxisAlignment.spaceAround], |
| /// [MainAxisAlignment.spaceBetween], or |
| /// [MainAxisAlignment.spaceEvenly], then the [spacing] parameter is |
| /// only used to see if the horizontal layout will overflow. |
| /// |
| /// Defaults to null. |
| /// |
| /// See also: |
| /// |
| /// * [overflowAlignment], the horizontal alignment of the [children] within |
| /// the vertical "overflow" layout. |
| /// |
| final MainAxisAlignment? alignment; |
| |
| /// The height of the gap between [children] in the vertical |
| /// "overflow" layout. |
| /// |
| /// This parameter is only used if the horizontal layout overflows, i.e. |
| /// if there isn't enough horizontal room for the [children] and [spacing]. |
| /// |
| /// Defaults to 0.0. |
| /// |
| /// See also: |
| /// |
| /// * [spacing], The width of the gap between each pair of children |
| /// for the default horizontal layout. |
| final double overflowSpacing; |
| |
| /// The horizontal alignment of the [children] within the vertical |
| /// "overflow" layout. |
| /// |
| /// This parameter is only used if the horizontal layout overflows, i.e. |
| /// if there isn't enough horizontal room for the [children] and [spacing]. |
| /// In that case the overflow bar will expand to fill the available |
| /// width and it will layout its [children] in a column. The |
| /// horizontal alignment of each child within that column is |
| /// defined by this parameter and the [textDirection]. If the |
| /// [textDirection] is [TextDirection.ltr] then each child will be |
| /// aligned with the left edge of the available space for |
| /// [OverflowBarAlignment.start], with the right edge of the |
| /// available space for [OverflowBarAlignment.end]. Similarly, if the |
| /// [textDirection] is [TextDirection.rtl] then each child will |
| /// be aligned with the right edge of the available space for |
| /// [OverflowBarAlignment.start], and with the left edge of the |
| /// available space for [OverflowBarAlignment.end]. For |
| /// [OverflowBarAlignment.center] each child is horizontally |
| /// centered within the available space. |
| /// |
| /// Defaults to [OverflowBarAlignment.start]. |
| /// |
| /// See also: |
| /// |
| /// * [alignment], which defines the [children]'s horizontal layout |
| /// (according to the same rules as for [Row.mainAxisAlignment]) when |
| /// the children, separated by [spacing], fit within the available space. |
| /// * [overflowDirection], which defines the order that the |
| /// [OverflowBar]'s children appear in, if the horizontal layout |
| /// overflows. |
| final OverflowBarAlignment overflowAlignment; |
| |
| /// Defines the order that the [children] appear in, if |
| /// the horizontal layout overflows. |
| /// |
| /// This parameter is only used if the horizontal layout overflows, i.e. |
| /// if there isn't enough horizontal room for the [children] and [spacing]. |
| /// |
| /// If the children do not fit into a single row, then they |
| /// are arranged in a column. The first child is at the top of the |
| /// column if this property is set to [VerticalDirection.down], since it |
| /// "starts" at the top and "ends" at the bottom. On the other hand, |
| /// the first child will be at the bottom of the column if this |
| /// property is set to [VerticalDirection.up], since it "starts" at the |
| /// bottom and "ends" at the top. |
| /// |
| /// Defaults to [VerticalDirection.down]. |
| /// |
| /// See also: |
| /// |
| /// * [overflowAlignment], which defines the horizontal alignment |
| /// of the children within the vertical "overflow" layout. |
| final VerticalDirection overflowDirection; |
| |
| /// Determines the order that the [children] appear in for the default |
| /// horizontal layout, and the interpretation of |
| /// [OverflowBarAlignment.start] and [OverflowBarAlignment.end] for |
| /// the vertical overflow layout. |
| /// |
| /// For the default horizontal layout, if [textDirection] is |
| /// [TextDirection.rtl] then the last child is laid out first. If |
| /// [textDirection] is [TextDirection.ltr] then the first child is |
| /// laid out first. |
| /// |
| /// If this parameter is null, then the value of |
| /// `Directionality.of(context)` is used. |
| /// |
| /// See also: |
| /// |
| /// * [overflowDirection], which defines the order that the |
| /// [OverflowBar]'s children appear in, if the horizontal layout |
| /// overflows. |
| /// * [Directionality], which defines the ambient directionality of |
| /// text and text-direction-sensitive render objects. |
| final TextDirection? textDirection; |
| |
| /// {@macro flutter.material.Material.clipBehavior} |
| /// |
| /// Defaults to [Clip.none], and must not be null. |
| final Clip clipBehavior; |
| |
| @override |
| RenderObject createRenderObject(BuildContext context) { |
| return _RenderOverflowBar( |
| spacing: spacing, |
| alignment: alignment, |
| overflowSpacing: overflowSpacing, |
| overflowAlignment: overflowAlignment, |
| overflowDirection: overflowDirection, |
| textDirection: textDirection ?? Directionality.of(context), |
| clipBehavior: clipBehavior, |
| ); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, RenderObject renderObject) { |
| (renderObject as _RenderOverflowBar) |
| ..spacing = spacing |
| ..alignment = alignment |
| ..overflowSpacing = overflowSpacing |
| ..overflowAlignment = overflowAlignment |
| ..overflowDirection = overflowDirection |
| ..textDirection = textDirection ?? Directionality.of(context) |
| ..clipBehavior = clipBehavior; |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DoubleProperty('spacing', spacing, defaultValue: 0)); |
| properties.add(EnumProperty<MainAxisAlignment>('alignment', alignment, defaultValue: null)); |
| properties.add(DoubleProperty('overflowSpacing', overflowSpacing, defaultValue: 0)); |
| properties.add(EnumProperty<OverflowBarAlignment>('overflowAlignment', overflowAlignment, defaultValue: OverflowBarAlignment.start)); |
| properties.add(EnumProperty<VerticalDirection>('overflowDirection', overflowDirection, defaultValue: VerticalDirection.down)); |
| properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); |
| } |
| } |
| |
| class _OverflowBarParentData extends ContainerBoxParentData<RenderBox> { } |
| |
| class _RenderOverflowBar extends RenderBox |
| with ContainerRenderObjectMixin<RenderBox, _OverflowBarParentData>, |
| RenderBoxContainerDefaultsMixin<RenderBox, _OverflowBarParentData> { |
| _RenderOverflowBar({ |
| List<RenderBox>? children, |
| double spacing = 0.0, |
| MainAxisAlignment? alignment, |
| double overflowSpacing = 0.0, |
| OverflowBarAlignment overflowAlignment = OverflowBarAlignment.start, |
| VerticalDirection overflowDirection = VerticalDirection.down, |
| required TextDirection textDirection, |
| Clip clipBehavior = Clip.none, |
| }) : assert(spacing != null), |
| assert(overflowSpacing != null), |
| assert(overflowAlignment != null), |
| assert(textDirection != null), |
| assert(clipBehavior != null), |
| _spacing = spacing, |
| _alignment = alignment, |
| _overflowSpacing = overflowSpacing, |
| _overflowAlignment = overflowAlignment, |
| _overflowDirection = overflowDirection, |
| _textDirection = textDirection, |
| _clipBehavior = clipBehavior { |
| addAll(children); |
| } |
| |
| double get spacing => _spacing; |
| double _spacing; |
| set spacing (double value) { |
| assert(value != null); |
| if (_spacing == value) { |
| return; |
| } |
| _spacing = value; |
| markNeedsLayout(); |
| } |
| |
| MainAxisAlignment? get alignment => _alignment; |
| MainAxisAlignment? _alignment; |
| set alignment (MainAxisAlignment? value) { |
| if (_alignment == value) { |
| return; |
| } |
| _alignment = value; |
| markNeedsLayout(); |
| } |
| |
| double get overflowSpacing => _overflowSpacing; |
| double _overflowSpacing; |
| set overflowSpacing (double value) { |
| assert(value != null); |
| if (_overflowSpacing == value) { |
| return; |
| } |
| _overflowSpacing = value; |
| markNeedsLayout(); |
| } |
| |
| OverflowBarAlignment get overflowAlignment => _overflowAlignment; |
| OverflowBarAlignment _overflowAlignment; |
| set overflowAlignment (OverflowBarAlignment value) { |
| assert(value != null); |
| if (_overflowAlignment == value) { |
| return; |
| } |
| _overflowAlignment = value; |
| markNeedsLayout(); |
| } |
| |
| VerticalDirection get overflowDirection => _overflowDirection; |
| VerticalDirection _overflowDirection; |
| set overflowDirection (VerticalDirection value) { |
| assert(value != null); |
| if (_overflowDirection == value) { |
| return; |
| } |
| _overflowDirection = value; |
| markNeedsLayout(); |
| } |
| |
| TextDirection get textDirection => _textDirection; |
| TextDirection _textDirection; |
| set textDirection(TextDirection value) { |
| if (_textDirection == value) { |
| return; |
| } |
| _textDirection = value; |
| markNeedsLayout(); |
| } |
| |
| Clip get clipBehavior => _clipBehavior; |
| Clip _clipBehavior = Clip.none; |
| set clipBehavior(Clip value) { |
| assert(value != null); |
| if (value == _clipBehavior) { |
| return; |
| } |
| _clipBehavior = value; |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| @override |
| void setupParentData(RenderBox child) { |
| if (child.parentData is! _OverflowBarParentData) { |
| child.parentData = _OverflowBarParentData(); |
| } |
| } |
| |
| @override |
| double computeMinIntrinsicHeight(double width) { |
| RenderBox? child = firstChild; |
| if (child == null) { |
| return 0; |
| } |
| double barWidth = 0.0; |
| while (child != null) { |
| barWidth += child.getMinIntrinsicWidth(double.infinity); |
| child = childAfter(child); |
| } |
| barWidth += spacing * (childCount - 1); |
| |
| double height = 0.0; |
| if (barWidth > width) { |
| child = firstChild; |
| while (child != null) { |
| height += child.getMinIntrinsicHeight(width); |
| child = childAfter(child); |
| } |
| return height + overflowSpacing * (childCount - 1); |
| } else { |
| child = firstChild; |
| while (child != null) { |
| height = math.max(height, child.getMinIntrinsicHeight(width)); |
| child = childAfter(child); |
| } |
| return height; |
| } |
| } |
| |
| @override |
| double computeMaxIntrinsicHeight(double width) { |
| RenderBox? child = firstChild; |
| if (child == null) { |
| return 0; |
| } |
| double barWidth = 0.0; |
| while (child != null) { |
| barWidth += child.getMinIntrinsicWidth(double.infinity); |
| child = childAfter(child); |
| } |
| barWidth += spacing * (childCount - 1); |
| |
| double height = 0.0; |
| if (barWidth > width) { |
| child = firstChild; |
| while (child != null) { |
| height += child.getMaxIntrinsicHeight(width); |
| child = childAfter(child); |
| } |
| return height + overflowSpacing * (childCount - 1); |
| } else { |
| child = firstChild; |
| while (child != null) { |
| height = math.max(height, child.getMaxIntrinsicHeight(width)); |
| child = childAfter(child); |
| } |
| return height; |
| } |
| } |
| |
| @override |
| double computeMinIntrinsicWidth(double height) { |
| RenderBox? child = firstChild; |
| if (child == null) { |
| return 0; |
| } |
| double width = 0.0; |
| while (child != null) { |
| width += child.getMinIntrinsicWidth(double.infinity); |
| child = childAfter(child); |
| } |
| return width + spacing * (childCount - 1); |
| } |
| |
| @override |
| double computeMaxIntrinsicWidth(double height) { |
| RenderBox? child = firstChild; |
| if (child == null) { |
| return 0; |
| } |
| double width = 0.0; |
| while (child != null) { |
| width += child.getMaxIntrinsicWidth(double.infinity); |
| child = childAfter(child); |
| } |
| return width + spacing * (childCount - 1); |
| } |
| |
| @override |
| double? computeDistanceToActualBaseline(TextBaseline baseline) { |
| return defaultComputeDistanceToHighestActualBaseline(baseline); |
| } |
| |
| @override |
| Size computeDryLayout(BoxConstraints constraints) { |
| RenderBox? child = firstChild; |
| if (child == null) { |
| return constraints.smallest; |
| } |
| final BoxConstraints childConstraints = constraints.loosen(); |
| double childrenWidth = 0.0; |
| double maxChildHeight = 0.0; |
| double y = 0.0; |
| while (child != null) { |
| final Size childSize = child.getDryLayout(childConstraints); |
| childrenWidth += childSize.width; |
| maxChildHeight = math.max(maxChildHeight, childSize.height); |
| y += childSize.height + overflowSpacing; |
| child = childAfter(child); |
| } |
| final double actualWidth = childrenWidth + spacing * (childCount - 1); |
| if (actualWidth > constraints.maxWidth) { |
| return constraints.constrain(Size(constraints.maxWidth, y - overflowSpacing)); |
| } else { |
| final double overallWidth = alignment == null ? actualWidth : constraints.maxWidth; |
| return constraints.constrain(Size(overallWidth, maxChildHeight)); |
| } |
| } |
| |
| @override |
| void performLayout() { |
| RenderBox? child = firstChild; |
| if (child == null) { |
| size = constraints.smallest; |
| return; |
| } |
| |
| final BoxConstraints childConstraints = constraints.loosen(); |
| double childrenWidth = 0; |
| double maxChildHeight = 0; |
| double maxChildWidth = 0; |
| |
| while (child != null) { |
| child.layout(childConstraints, parentUsesSize: true); |
| childrenWidth += child.size.width; |
| maxChildHeight = math.max(maxChildHeight, child.size.height); |
| maxChildWidth = math.max(maxChildWidth, child.size.width); |
| child = childAfter(child); |
| } |
| |
| final bool rtl = textDirection == TextDirection.rtl; |
| final double actualWidth = childrenWidth + spacing * (childCount - 1); |
| |
| if (actualWidth > constraints.maxWidth) { |
| // Overflow vertical layout |
| child = overflowDirection == VerticalDirection.down ? firstChild : lastChild; |
| RenderBox? nextChild() => overflowDirection == VerticalDirection.down ? childAfter(child!) : childBefore(child!); |
| double y = 0; |
| while (child != null) { |
| final _OverflowBarParentData childParentData = child.parentData! as _OverflowBarParentData; |
| double x = 0; |
| switch (overflowAlignment) { |
| case OverflowBarAlignment.start: |
| x = rtl ? constraints.maxWidth - child.size.width : 0; |
| break; |
| case OverflowBarAlignment.center: |
| x = (constraints.maxWidth - child.size.width) / 2; |
| break; |
| case OverflowBarAlignment.end: |
| x = rtl ? 0 : constraints.maxWidth - child.size.width; |
| break; |
| } |
| assert(x != null); |
| childParentData.offset = Offset(x, y); |
| y += child.size.height + overflowSpacing; |
| child = nextChild(); |
| } |
| size = constraints.constrain(Size(constraints.maxWidth, y - overflowSpacing)); |
| } else { |
| // Default horizontal layout |
| child = firstChild; |
| final double firstChildWidth = child!.size.width; |
| final double overallWidth = alignment == null ? actualWidth : constraints.maxWidth; |
| size = constraints.constrain(Size(overallWidth, maxChildHeight)); |
| |
| late double x; // initial value: origin of the first child |
| double layoutSpacing = spacing; // space between children |
| switch (alignment) { |
| case null: |
| x = rtl ? size.width - firstChildWidth : 0; |
| break; |
| case MainAxisAlignment.start: |
| x = rtl ? size.width - firstChildWidth : 0; |
| break; |
| case MainAxisAlignment.center: |
| final double halfRemainingWidth = (size.width - actualWidth) / 2; |
| x = rtl ? size.width - halfRemainingWidth - firstChildWidth : halfRemainingWidth; |
| break; |
| case MainAxisAlignment.end: |
| x = rtl ? actualWidth - firstChildWidth : size.width - actualWidth; |
| break; |
| case MainAxisAlignment.spaceBetween: |
| layoutSpacing = (size.width - childrenWidth) / (childCount - 1); |
| x = rtl ? size.width - firstChildWidth : 0; |
| break; |
| case MainAxisAlignment.spaceAround: |
| layoutSpacing = childCount > 0 ? (size.width - childrenWidth) / childCount : 0; |
| x = rtl ? size.width - layoutSpacing / 2 - firstChildWidth : layoutSpacing / 2; |
| break; |
| case MainAxisAlignment.spaceEvenly: |
| layoutSpacing = (size.width - childrenWidth) / (childCount + 1); |
| x = rtl ? size.width - layoutSpacing - firstChildWidth : layoutSpacing; |
| break; |
| } |
| |
| while (child != null) { |
| final _OverflowBarParentData childParentData = child.parentData! as _OverflowBarParentData; |
| childParentData.offset = Offset(x, (maxChildHeight - child.size.height) / 2); |
| // x is the horizontal origin of child. To advance x to the next child's |
| // origin for LTR: add the width of the current child. To advance x to |
| // the origin of the next child for RTL: subtract the width of the next |
| // child (if there is one). |
| if (!rtl) { |
| x += child.size.width + layoutSpacing; |
| } |
| child = childAfter(child); |
| if (rtl && child != null) { |
| x -= child.size.width + layoutSpacing; |
| } |
| } |
| } |
| } |
| |
| @override |
| bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
| return defaultHitTestChildren(result, position: position); |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| defaultPaint(context, offset); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DoubleProperty('spacing', spacing, defaultValue: 0)); |
| properties.add(DoubleProperty('overflowSpacing', overflowSpacing, defaultValue: 0)); |
| properties.add(EnumProperty<OverflowBarAlignment>('overflowAlignment', overflowAlignment, defaultValue: OverflowBarAlignment.start)); |
| properties.add(EnumProperty<VerticalDirection>('overflowDirection', overflowDirection, defaultValue: VerticalDirection.down)); |
| properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); |
| } |
| } |