1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:math' as math; |
6 | |
7 | import 'package:flutter/rendering.dart'; |
8 | |
9 | import 'basic.dart'; |
10 | import 'framework.dart'; |
11 | |
12 | /// Defines the horizontal alignment of [OverflowBar] children |
13 | /// when they're laid out in an overflow column. |
14 | /// |
15 | /// This value must be interpreted relative to the ambient |
16 | /// [TextDirection]. |
17 | enum OverflowBarAlignment { |
18 | /// Each child is left-aligned for [TextDirection.ltr], |
19 | /// right-aligned for [TextDirection.rtl]. |
20 | start, |
21 | |
22 | /// Each child is right-aligned for [TextDirection.ltr], |
23 | /// left-aligned for [TextDirection.rtl]. |
24 | end, |
25 | |
26 | /// Each child is horizontally centered. |
27 | center, |
28 | } |
29 | |
30 | /// A widget that lays out its [children] in a row unless they |
31 | /// "overflow" the available horizontal space, in which case it lays |
32 | /// them out in a column instead. |
33 | /// |
34 | /// This widget's width will expand to contain its children and the |
35 | /// specified [spacing] until it overflows. The overflow column will |
36 | /// consume all of the available width. The [overflowAlignment] |
37 | /// defines how each child will be aligned within the overflow column |
38 | /// and the [overflowSpacing] defines the gap between each child. |
39 | /// |
40 | /// The order that the children appear in the horizontal layout |
41 | /// is defined by the [textDirection], just like the [Row] widget. |
42 | /// If the layout overflows, then children's order within their |
43 | /// column is specified by [overflowDirection] instead. |
44 | /// |
45 | /// {@tool dartpad} |
46 | /// This example defines a simple approximation of a dialog |
47 | /// layout, where the layout of the dialog's action buttons are |
48 | /// defined by an [OverflowBar]. The content is wrapped in a |
49 | /// [SingleChildScrollView], so that if overflow occurs, the |
50 | /// action buttons will still be accessible by scrolling, |
51 | /// no matter how much vertical space is available. |
52 | /// |
53 | /// ** See code in examples/api/lib/widgets/overflow_bar/overflow_bar.0.dart ** |
54 | /// {@end-tool} |
55 | class OverflowBar extends MultiChildRenderObjectWidget { |
56 | /// Constructs an OverflowBar. |
57 | const OverflowBar({ |
58 | super.key, |
59 | this.spacing = 0.0, |
60 | this.alignment, |
61 | this.overflowSpacing = 0.0, |
62 | this.overflowAlignment = OverflowBarAlignment.start, |
63 | this.overflowDirection = VerticalDirection.down, |
64 | this.textDirection, |
65 | this.clipBehavior = Clip.none, |
66 | super.children, |
67 | }); |
68 | |
69 | /// The width of the gap between [children] for the default |
70 | /// horizontal layout. |
71 | /// |
72 | /// If the horizontal layout overflows, then [overflowSpacing] is |
73 | /// used instead. |
74 | /// |
75 | /// Defaults to 0.0. |
76 | final double spacing; |
77 | |
78 | /// Defines the [children]'s horizontal layout according to the same |
79 | /// rules as for [Row.mainAxisAlignment]. |
80 | /// |
81 | /// If this property is non-null, and the [children], separated by |
82 | /// [spacing], fit within the available width, then the overflow |
83 | /// bar will be as wide as possible. If the children do not fit |
84 | /// within the available width, then this property is ignored and |
85 | /// [overflowAlignment] applies instead. |
86 | /// |
87 | /// If this property is null (the default) then the overflow bar |
88 | /// will be no wider than needed to layout the [children] separated |
89 | /// by [spacing], modulo the incoming constraints. |
90 | /// |
91 | /// If [alignment] is one of [MainAxisAlignment.spaceAround], |
92 | /// [MainAxisAlignment.spaceBetween], or |
93 | /// [MainAxisAlignment.spaceEvenly], then the [spacing] parameter is |
94 | /// only used to see if the horizontal layout will overflow. |
95 | /// |
96 | /// Defaults to null. |
97 | /// |
98 | /// See also: |
99 | /// |
100 | /// * [overflowAlignment], the horizontal alignment of the [children] within |
101 | /// the vertical "overflow" layout. |
102 | /// |
103 | final MainAxisAlignment? alignment; |
104 | |
105 | /// The height of the gap between [children] in the vertical |
106 | /// "overflow" layout. |
107 | /// |
108 | /// This parameter is only used if the horizontal layout overflows, i.e. |
109 | /// if there isn't enough horizontal room for the [children] and [spacing]. |
110 | /// |
111 | /// Defaults to 0.0. |
112 | /// |
113 | /// See also: |
114 | /// |
115 | /// * [spacing], The width of the gap between each pair of children |
116 | /// for the default horizontal layout. |
117 | final double overflowSpacing; |
118 | |
119 | /// The horizontal alignment of the [children] within the vertical |
120 | /// "overflow" layout. |
121 | /// |
122 | /// This parameter is only used if the horizontal layout overflows, i.e. |
123 | /// if there isn't enough horizontal room for the [children] and [spacing]. |
124 | /// In that case the overflow bar will expand to fill the available |
125 | /// width and it will layout its [children] in a column. The |
126 | /// horizontal alignment of each child within that column is |
127 | /// defined by this parameter and the [textDirection]. If the |
128 | /// [textDirection] is [TextDirection.ltr] then each child will be |
129 | /// aligned with the left edge of the available space for |
130 | /// [OverflowBarAlignment.start], with the right edge of the |
131 | /// available space for [OverflowBarAlignment.end]. Similarly, if the |
132 | /// [textDirection] is [TextDirection.rtl] then each child will |
133 | /// be aligned with the right edge of the available space for |
134 | /// [OverflowBarAlignment.start], and with the left edge of the |
135 | /// available space for [OverflowBarAlignment.end]. For |
136 | /// [OverflowBarAlignment.center] each child is horizontally |
137 | /// centered within the available space. |
138 | /// |
139 | /// Defaults to [OverflowBarAlignment.start]. |
140 | /// |
141 | /// See also: |
142 | /// |
143 | /// * [alignment], which defines the [children]'s horizontal layout |
144 | /// (according to the same rules as for [Row.mainAxisAlignment]) when |
145 | /// the children, separated by [spacing], fit within the available space. |
146 | /// * [overflowDirection], which defines the order that the |
147 | /// [OverflowBar]'s children appear in, if the horizontal layout |
148 | /// overflows. |
149 | final OverflowBarAlignment overflowAlignment; |
150 | |
151 | /// Defines the order that the [children] appear in, if |
152 | /// the horizontal layout overflows. |
153 | /// |
154 | /// This parameter is only used if the horizontal layout overflows, i.e. |
155 | /// if there isn't enough horizontal room for the [children] and [spacing]. |
156 | /// |
157 | /// If the children do not fit into a single row, then they |
158 | /// are arranged in a column. The first child is at the top of the |
159 | /// column if this property is set to [VerticalDirection.down], since it |
160 | /// "starts" at the top and "ends" at the bottom. On the other hand, |
161 | /// the first child will be at the bottom of the column if this |
162 | /// property is set to [VerticalDirection.up], since it "starts" at the |
163 | /// bottom and "ends" at the top. |
164 | /// |
165 | /// Defaults to [VerticalDirection.down]. |
166 | /// |
167 | /// See also: |
168 | /// |
169 | /// * [overflowAlignment], which defines the horizontal alignment |
170 | /// of the children within the vertical "overflow" layout. |
171 | final VerticalDirection overflowDirection; |
172 | |
173 | /// Determines the order that the [children] appear in for the default |
174 | /// horizontal layout, and the interpretation of |
175 | /// [OverflowBarAlignment.start] and [OverflowBarAlignment.end] for |
176 | /// the vertical overflow layout. |
177 | /// |
178 | /// For the default horizontal layout, if [textDirection] is |
179 | /// [TextDirection.rtl] then the last child is laid out first. If |
180 | /// [textDirection] is [TextDirection.ltr] then the first child is |
181 | /// laid out first. |
182 | /// |
183 | /// If this parameter is null, then the value of |
184 | /// `Directionality.of(context)` is used. |
185 | /// |
186 | /// See also: |
187 | /// |
188 | /// * [overflowDirection], which defines the order that the |
189 | /// [OverflowBar]'s children appear in, if the horizontal layout |
190 | /// overflows. |
191 | /// * [Directionality], which defines the ambient directionality of |
192 | /// text and text-direction-sensitive render objects. |
193 | final TextDirection? textDirection; |
194 | |
195 | /// {@macro flutter.material.Material.clipBehavior} |
196 | /// |
197 | /// Defaults to [Clip.none]. |
198 | final Clip clipBehavior; |
199 | |
200 | @override |
201 | RenderObject createRenderObject(BuildContext context) { |
202 | return _RenderOverflowBar( |
203 | spacing: spacing, |
204 | alignment: alignment, |
205 | overflowSpacing: overflowSpacing, |
206 | overflowAlignment: overflowAlignment, |
207 | overflowDirection: overflowDirection, |
208 | textDirection: textDirection ?? Directionality.of(context), |
209 | clipBehavior: clipBehavior, |
210 | ); |
211 | } |
212 | |
213 | @override |
214 | void updateRenderObject(BuildContext context, RenderObject renderObject) { |
215 | (renderObject as _RenderOverflowBar) |
216 | ..spacing = spacing |
217 | ..alignment = alignment |
218 | ..overflowSpacing = overflowSpacing |
219 | ..overflowAlignment = overflowAlignment |
220 | ..overflowDirection = overflowDirection |
221 | ..textDirection = textDirection ?? Directionality.of(context) |
222 | ..clipBehavior = clipBehavior; |
223 | } |
224 | |
225 | @override |
226 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
227 | super.debugFillProperties(properties); |
228 | properties.add(DoubleProperty('spacing' , spacing, defaultValue: 0)); |
229 | properties.add(EnumProperty<MainAxisAlignment>('alignment' , alignment, defaultValue: null)); |
230 | properties.add(DoubleProperty('overflowSpacing' , overflowSpacing, defaultValue: 0)); |
231 | properties.add(EnumProperty<OverflowBarAlignment>('overflowAlignment' , overflowAlignment, defaultValue: OverflowBarAlignment.start)); |
232 | properties.add(EnumProperty<VerticalDirection>('overflowDirection' , overflowDirection, defaultValue: VerticalDirection.down)); |
233 | properties.add(EnumProperty<TextDirection>('textDirection' , textDirection, defaultValue: null)); |
234 | } |
235 | } |
236 | |
237 | class _OverflowBarParentData extends ContainerBoxParentData<RenderBox> { } |
238 | |
239 | class _RenderOverflowBar extends RenderBox |
240 | with ContainerRenderObjectMixin<RenderBox, _OverflowBarParentData>, |
241 | RenderBoxContainerDefaultsMixin<RenderBox, _OverflowBarParentData> { |
242 | _RenderOverflowBar({ |
243 | List<RenderBox>? children, |
244 | double spacing = 0.0, |
245 | MainAxisAlignment? alignment, |
246 | double overflowSpacing = 0.0, |
247 | OverflowBarAlignment overflowAlignment = OverflowBarAlignment.start, |
248 | VerticalDirection overflowDirection = VerticalDirection.down, |
249 | required TextDirection textDirection, |
250 | Clip clipBehavior = Clip.none, |
251 | }) : _spacing = spacing, |
252 | _alignment = alignment, |
253 | _overflowSpacing = overflowSpacing, |
254 | _overflowAlignment = overflowAlignment, |
255 | _overflowDirection = overflowDirection, |
256 | _textDirection = textDirection, |
257 | _clipBehavior = clipBehavior { |
258 | addAll(children); |
259 | } |
260 | |
261 | double get spacing => _spacing; |
262 | double _spacing; |
263 | set spacing (double value) { |
264 | if (_spacing == value) { |
265 | return; |
266 | } |
267 | _spacing = value; |
268 | markNeedsLayout(); |
269 | } |
270 | |
271 | MainAxisAlignment? get alignment => _alignment; |
272 | MainAxisAlignment? _alignment; |
273 | set alignment (MainAxisAlignment? value) { |
274 | if (_alignment == value) { |
275 | return; |
276 | } |
277 | _alignment = value; |
278 | markNeedsLayout(); |
279 | } |
280 | |
281 | double get overflowSpacing => _overflowSpacing; |
282 | double _overflowSpacing; |
283 | set overflowSpacing (double value) { |
284 | if (_overflowSpacing == value) { |
285 | return; |
286 | } |
287 | _overflowSpacing = value; |
288 | markNeedsLayout(); |
289 | } |
290 | |
291 | OverflowBarAlignment get overflowAlignment => _overflowAlignment; |
292 | OverflowBarAlignment _overflowAlignment; |
293 | set overflowAlignment (OverflowBarAlignment value) { |
294 | if (_overflowAlignment == value) { |
295 | return; |
296 | } |
297 | _overflowAlignment = value; |
298 | markNeedsLayout(); |
299 | } |
300 | |
301 | VerticalDirection get overflowDirection => _overflowDirection; |
302 | VerticalDirection _overflowDirection; |
303 | set overflowDirection (VerticalDirection value) { |
304 | if (_overflowDirection == value) { |
305 | return; |
306 | } |
307 | _overflowDirection = value; |
308 | markNeedsLayout(); |
309 | } |
310 | |
311 | TextDirection get textDirection => _textDirection; |
312 | TextDirection _textDirection; |
313 | set textDirection(TextDirection value) { |
314 | if (_textDirection == value) { |
315 | return; |
316 | } |
317 | _textDirection = value; |
318 | markNeedsLayout(); |
319 | } |
320 | |
321 | Clip get clipBehavior => _clipBehavior; |
322 | Clip _clipBehavior = Clip.none; |
323 | set clipBehavior(Clip value) { |
324 | if (value == _clipBehavior) { |
325 | return; |
326 | } |
327 | _clipBehavior = value; |
328 | markNeedsPaint(); |
329 | markNeedsSemanticsUpdate(); |
330 | } |
331 | |
332 | @override |
333 | void setupParentData(RenderBox child) { |
334 | if (child.parentData is! _OverflowBarParentData) { |
335 | child.parentData = _OverflowBarParentData(); |
336 | } |
337 | } |
338 | |
339 | @override |
340 | double computeMinIntrinsicHeight(double width) { |
341 | RenderBox? child = firstChild; |
342 | if (child == null) { |
343 | return 0; |
344 | } |
345 | double barWidth = 0.0; |
346 | while (child != null) { |
347 | barWidth += child.getMinIntrinsicWidth(double.infinity); |
348 | child = childAfter(child); |
349 | } |
350 | barWidth += spacing * (childCount - 1); |
351 | |
352 | double height = 0.0; |
353 | if (barWidth > width) { |
354 | child = firstChild; |
355 | while (child != null) { |
356 | height += child.getMinIntrinsicHeight(width); |
357 | child = childAfter(child); |
358 | } |
359 | return height + overflowSpacing * (childCount - 1); |
360 | } else { |
361 | child = firstChild; |
362 | while (child != null) { |
363 | height = math.max(height, child.getMinIntrinsicHeight(width)); |
364 | child = childAfter(child); |
365 | } |
366 | return height; |
367 | } |
368 | } |
369 | |
370 | @override |
371 | double computeMaxIntrinsicHeight(double width) { |
372 | RenderBox? child = firstChild; |
373 | if (child == null) { |
374 | return 0; |
375 | } |
376 | double barWidth = 0.0; |
377 | while (child != null) { |
378 | barWidth += child.getMinIntrinsicWidth(double.infinity); |
379 | child = childAfter(child); |
380 | } |
381 | barWidth += spacing * (childCount - 1); |
382 | |
383 | double height = 0.0; |
384 | if (barWidth > width) { |
385 | child = firstChild; |
386 | while (child != null) { |
387 | height += child.getMaxIntrinsicHeight(width); |
388 | child = childAfter(child); |
389 | } |
390 | return height + overflowSpacing * (childCount - 1); |
391 | } else { |
392 | child = firstChild; |
393 | while (child != null) { |
394 | height = math.max(height, child.getMaxIntrinsicHeight(width)); |
395 | child = childAfter(child); |
396 | } |
397 | return height; |
398 | } |
399 | } |
400 | |
401 | @override |
402 | double computeMinIntrinsicWidth(double height) { |
403 | RenderBox? child = firstChild; |
404 | if (child == null) { |
405 | return 0; |
406 | } |
407 | double width = 0.0; |
408 | while (child != null) { |
409 | width += child.getMinIntrinsicWidth(double.infinity); |
410 | child = childAfter(child); |
411 | } |
412 | return width + spacing * (childCount - 1); |
413 | } |
414 | |
415 | @override |
416 | double computeMaxIntrinsicWidth(double height) { |
417 | RenderBox? child = firstChild; |
418 | if (child == null) { |
419 | return 0; |
420 | } |
421 | double width = 0.0; |
422 | while (child != null) { |
423 | width += child.getMaxIntrinsicWidth(double.infinity); |
424 | child = childAfter(child); |
425 | } |
426 | return width + spacing * (childCount - 1); |
427 | } |
428 | |
429 | @override |
430 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
431 | return defaultComputeDistanceToHighestActualBaseline(baseline); |
432 | } |
433 | |
434 | @override |
435 | Size computeDryLayout(BoxConstraints constraints) { |
436 | RenderBox? child = firstChild; |
437 | if (child == null) { |
438 | return constraints.smallest; |
439 | } |
440 | final BoxConstraints childConstraints = constraints.loosen(); |
441 | double childrenWidth = 0.0; |
442 | double maxChildHeight = 0.0; |
443 | double y = 0.0; |
444 | while (child != null) { |
445 | final Size childSize = child.getDryLayout(childConstraints); |
446 | childrenWidth += childSize.width; |
447 | maxChildHeight = math.max(maxChildHeight, childSize.height); |
448 | y += childSize.height + overflowSpacing; |
449 | child = childAfter(child); |
450 | } |
451 | final double actualWidth = childrenWidth + spacing * (childCount - 1); |
452 | if (actualWidth > constraints.maxWidth) { |
453 | return constraints.constrain(Size(constraints.maxWidth, y - overflowSpacing)); |
454 | } else { |
455 | final double overallWidth = alignment == null ? actualWidth : constraints.maxWidth; |
456 | return constraints.constrain(Size(overallWidth, maxChildHeight)); |
457 | } |
458 | } |
459 | |
460 | @override |
461 | void performLayout() { |
462 | RenderBox? child = firstChild; |
463 | if (child == null) { |
464 | size = constraints.smallest; |
465 | return; |
466 | } |
467 | |
468 | final BoxConstraints childConstraints = constraints.loosen(); |
469 | double childrenWidth = 0; |
470 | double maxChildHeight = 0; |
471 | double maxChildWidth = 0; |
472 | |
473 | while (child != null) { |
474 | child.layout(childConstraints, parentUsesSize: true); |
475 | childrenWidth += child.size.width; |
476 | maxChildHeight = math.max(maxChildHeight, child.size.height); |
477 | maxChildWidth = math.max(maxChildWidth, child.size.width); |
478 | child = childAfter(child); |
479 | } |
480 | |
481 | final bool rtl = textDirection == TextDirection.rtl; |
482 | final double actualWidth = childrenWidth + spacing * (childCount - 1); |
483 | |
484 | if (actualWidth > constraints.maxWidth) { |
485 | // Overflow vertical layout |
486 | child = overflowDirection == VerticalDirection.down ? firstChild : lastChild; |
487 | RenderBox? nextChild() => overflowDirection == VerticalDirection.down ? childAfter(child!) : childBefore(child!); |
488 | double y = 0; |
489 | while (child != null) { |
490 | final _OverflowBarParentData childParentData = child.parentData! as _OverflowBarParentData; |
491 | double x = 0; |
492 | switch (overflowAlignment) { |
493 | case OverflowBarAlignment.start: |
494 | x = rtl ? constraints.maxWidth - child.size.width : 0; |
495 | case OverflowBarAlignment.center: |
496 | x = (constraints.maxWidth - child.size.width) / 2; |
497 | case OverflowBarAlignment.end: |
498 | x = rtl ? 0 : constraints.maxWidth - child.size.width; |
499 | } |
500 | childParentData.offset = Offset(x, y); |
501 | y += child.size.height + overflowSpacing; |
502 | child = nextChild(); |
503 | } |
504 | size = constraints.constrain(Size(constraints.maxWidth, y - overflowSpacing)); |
505 | } else { |
506 | // Default horizontal layout |
507 | child = firstChild; |
508 | final double firstChildWidth = child!.size.width; |
509 | final double overallWidth = alignment == null ? actualWidth : constraints.maxWidth; |
510 | size = constraints.constrain(Size(overallWidth, maxChildHeight)); |
511 | |
512 | late double x; // initial value: origin of the first child |
513 | double layoutSpacing = spacing; // space between children |
514 | switch (alignment) { |
515 | case null: |
516 | x = rtl ? size.width - firstChildWidth : 0; |
517 | case MainAxisAlignment.start: |
518 | x = rtl ? size.width - firstChildWidth : 0; |
519 | case MainAxisAlignment.center: |
520 | final double halfRemainingWidth = (size.width - actualWidth) / 2; |
521 | x = rtl ? size.width - halfRemainingWidth - firstChildWidth : halfRemainingWidth; |
522 | case MainAxisAlignment.end: |
523 | x = rtl ? actualWidth - firstChildWidth : size.width - actualWidth; |
524 | case MainAxisAlignment.spaceBetween: |
525 | layoutSpacing = (size.width - childrenWidth) / (childCount - 1); |
526 | x = rtl ? size.width - firstChildWidth : 0; |
527 | case MainAxisAlignment.spaceAround: |
528 | layoutSpacing = childCount > 0 ? (size.width - childrenWidth) / childCount : 0; |
529 | x = rtl ? size.width - layoutSpacing / 2 - firstChildWidth : layoutSpacing / 2; |
530 | case MainAxisAlignment.spaceEvenly: |
531 | layoutSpacing = (size.width - childrenWidth) / (childCount + 1); |
532 | x = rtl ? size.width - layoutSpacing - firstChildWidth : layoutSpacing; |
533 | } |
534 | |
535 | while (child != null) { |
536 | final _OverflowBarParentData childParentData = child.parentData! as _OverflowBarParentData; |
537 | childParentData.offset = Offset(x, (maxChildHeight - child.size.height) / 2); |
538 | // x is the horizontal origin of child. To advance x to the next child's |
539 | // origin for LTR: add the width of the current child. To advance x to |
540 | // the origin of the next child for RTL: subtract the width of the next |
541 | // child (if there is one). |
542 | if (!rtl) { |
543 | x += child.size.width + layoutSpacing; |
544 | } |
545 | child = childAfter(child); |
546 | if (rtl && child != null) { |
547 | x -= child.size.width + layoutSpacing; |
548 | } |
549 | } |
550 | } |
551 | } |
552 | |
553 | @override |
554 | bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
555 | return defaultHitTestChildren(result, position: position); |
556 | } |
557 | |
558 | @override |
559 | void paint(PaintingContext context, Offset offset) { |
560 | defaultPaint(context, offset); |
561 | } |
562 | |
563 | @override |
564 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
565 | super.debugFillProperties(properties); |
566 | properties.add(DoubleProperty('spacing' , spacing, defaultValue: 0)); |
567 | properties.add(DoubleProperty('overflowSpacing' , overflowSpacing, defaultValue: 0)); |
568 | properties.add(EnumProperty<OverflowBarAlignment>('overflowAlignment' , overflowAlignment, defaultValue: OverflowBarAlignment.start)); |
569 | properties.add(EnumProperty<VerticalDirection>('overflowDirection' , overflowDirection, defaultValue: VerticalDirection.down)); |
570 | properties.add(EnumProperty<TextDirection>('textDirection' , textDirection, defaultValue: null)); |
571 | } |
572 | } |
573 | |