blob: f5fd4ba9e197e472e791afcf54f1a88cd7216ff9 [file] [log] [blame]
// 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));
}
}