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/foundation.dart'; |
8 | |
9 | import 'box.dart'; |
10 | import 'debug.dart'; |
11 | import 'debug_overflow_indicator.dart'; |
12 | import 'layer.dart'; |
13 | import 'object.dart'; |
14 | import 'stack.dart' show RelativeRect; |
15 | |
16 | /// Signature for a function that transforms a [BoxConstraints] to another |
17 | /// [BoxConstraints]. |
18 | /// |
19 | /// Used by [RenderConstraintsTransformBox] and [ConstraintsTransformBox]. |
20 | /// Typically the caller requires the returned [BoxConstraints] to be |
21 | /// [BoxConstraints.isNormalized]. |
22 | typedef BoxConstraintsTransform = BoxConstraints Function(BoxConstraints constraints); |
23 | |
24 | /// Abstract class for one-child-layout render boxes that provide control over |
25 | /// the child's position. |
26 | abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> { |
27 | /// Initializes the [child] property for subclasses. |
28 | RenderShiftedBox(RenderBox? child) { |
29 | this.child = child; |
30 | } |
31 | |
32 | @override |
33 | double computeMinIntrinsicWidth(double height) { |
34 | return child?.getMinIntrinsicWidth(height) ?? 0.0; |
35 | } |
36 | |
37 | @override |
38 | double computeMaxIntrinsicWidth(double height) { |
39 | return child?.getMaxIntrinsicWidth(height) ?? 0.0; |
40 | } |
41 | |
42 | @override |
43 | double computeMinIntrinsicHeight(double width) { |
44 | return child?.getMinIntrinsicHeight(width) ?? 0.0; |
45 | } |
46 | |
47 | @override |
48 | double computeMaxIntrinsicHeight(double width) { |
49 | return child?.getMaxIntrinsicHeight(width) ?? 0.0; |
50 | } |
51 | |
52 | @override |
53 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
54 | double? result; |
55 | final RenderBox? child = this.child; |
56 | if (child != null) { |
57 | assert(!debugNeedsLayout); |
58 | result = child.getDistanceToActualBaseline(baseline); |
59 | final BoxParentData childParentData = child.parentData! as BoxParentData; |
60 | if (result != null) { |
61 | result += childParentData.offset.dy; |
62 | } |
63 | } else { |
64 | result = super.computeDistanceToActualBaseline(baseline); |
65 | } |
66 | return result; |
67 | } |
68 | |
69 | @override |
70 | void paint(PaintingContext context, Offset offset) { |
71 | final RenderBox? child = this.child; |
72 | if (child != null) { |
73 | final BoxParentData childParentData = child.parentData! as BoxParentData; |
74 | context.paintChild(child, childParentData.offset + offset); |
75 | } |
76 | } |
77 | |
78 | @override |
79 | bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
80 | final RenderBox? child = this.child; |
81 | if (child != null) { |
82 | final BoxParentData childParentData = child.parentData! as BoxParentData; |
83 | return result.addWithPaintOffset( |
84 | offset: childParentData.offset, |
85 | position: position, |
86 | hitTest: (BoxHitTestResult result, Offset transformed) { |
87 | assert(transformed == position - childParentData.offset); |
88 | return child.hitTest(result, position: transformed); |
89 | }, |
90 | ); |
91 | } |
92 | return false; |
93 | } |
94 | } |
95 | |
96 | /// Insets its child by the given padding. |
97 | /// |
98 | /// When passing layout constraints to its child, padding shrinks the |
99 | /// constraints by the given padding, causing the child to layout at a smaller |
100 | /// size. Padding then sizes itself to its child's size, inflated by the |
101 | /// padding, effectively creating empty space around the child. |
102 | class RenderPadding extends RenderShiftedBox { |
103 | /// Creates a render object that insets its child. |
104 | /// |
105 | /// The [padding] argument must have non-negative insets. |
106 | RenderPadding({ |
107 | required EdgeInsetsGeometry padding, |
108 | TextDirection? textDirection, |
109 | RenderBox? child, |
110 | }) : assert(padding.isNonNegative), |
111 | _textDirection = textDirection, |
112 | _padding = padding, |
113 | super(child); |
114 | |
115 | EdgeInsets? _resolvedPadding; |
116 | |
117 | void _resolve() { |
118 | if (_resolvedPadding != null) { |
119 | return; |
120 | } |
121 | _resolvedPadding = padding.resolve(textDirection); |
122 | assert(_resolvedPadding!.isNonNegative); |
123 | } |
124 | |
125 | void _markNeedResolution() { |
126 | _resolvedPadding = null; |
127 | markNeedsLayout(); |
128 | } |
129 | |
130 | /// The amount to pad the child in each dimension. |
131 | /// |
132 | /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection] |
133 | /// must not be null. |
134 | EdgeInsetsGeometry get padding => _padding; |
135 | EdgeInsetsGeometry _padding; |
136 | set padding(EdgeInsetsGeometry value) { |
137 | assert(value.isNonNegative); |
138 | if (_padding == value) { |
139 | return; |
140 | } |
141 | _padding = value; |
142 | _markNeedResolution(); |
143 | } |
144 | |
145 | /// The text direction with which to resolve [padding]. |
146 | /// |
147 | /// This may be changed to null, but only after the [padding] has been changed |
148 | /// to a value that does not depend on the direction. |
149 | TextDirection? get textDirection => _textDirection; |
150 | TextDirection? _textDirection; |
151 | set textDirection(TextDirection? value) { |
152 | if (_textDirection == value) { |
153 | return; |
154 | } |
155 | _textDirection = value; |
156 | _markNeedResolution(); |
157 | } |
158 | |
159 | @override |
160 | double computeMinIntrinsicWidth(double height) { |
161 | _resolve(); |
162 | final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; |
163 | final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; |
164 | if (child != null) { |
165 | // Relies on double.infinity absorption. |
166 | return child!.getMinIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding; |
167 | } |
168 | return totalHorizontalPadding; |
169 | } |
170 | |
171 | @override |
172 | double computeMaxIntrinsicWidth(double height) { |
173 | _resolve(); |
174 | final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; |
175 | final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; |
176 | if (child != null) { |
177 | // Relies on double.infinity absorption. |
178 | return child!.getMaxIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding; |
179 | } |
180 | return totalHorizontalPadding; |
181 | } |
182 | |
183 | @override |
184 | double computeMinIntrinsicHeight(double width) { |
185 | _resolve(); |
186 | final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; |
187 | final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; |
188 | if (child != null) { |
189 | // Relies on double.infinity absorption. |
190 | return child!.getMinIntrinsicHeight(math.max(0.0, width - totalHorizontalPadding)) + totalVerticalPadding; |
191 | } |
192 | return totalVerticalPadding; |
193 | } |
194 | |
195 | @override |
196 | double computeMaxIntrinsicHeight(double width) { |
197 | _resolve(); |
198 | final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; |
199 | final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; |
200 | if (child != null) { |
201 | // Relies on double.infinity absorption. |
202 | return child!.getMaxIntrinsicHeight(math.max(0.0, width - totalHorizontalPadding)) + totalVerticalPadding; |
203 | } |
204 | return totalVerticalPadding; |
205 | } |
206 | |
207 | @override |
208 | @protected |
209 | Size computeDryLayout(covariant BoxConstraints constraints) { |
210 | _resolve(); |
211 | assert(_resolvedPadding != null); |
212 | if (child == null) { |
213 | return constraints.constrain(Size( |
214 | _resolvedPadding!.left + _resolvedPadding!.right, |
215 | _resolvedPadding!.top + _resolvedPadding!.bottom, |
216 | )); |
217 | } |
218 | final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding!); |
219 | final Size childSize = child!.getDryLayout(innerConstraints); |
220 | return constraints.constrain(Size( |
221 | _resolvedPadding!.left + childSize.width + _resolvedPadding!.right, |
222 | _resolvedPadding!.top + childSize.height + _resolvedPadding!.bottom, |
223 | )); |
224 | } |
225 | |
226 | @override |
227 | void performLayout() { |
228 | final BoxConstraints constraints = this.constraints; |
229 | _resolve(); |
230 | assert(_resolvedPadding != null); |
231 | if (child == null) { |
232 | size = constraints.constrain(Size( |
233 | _resolvedPadding!.left + _resolvedPadding!.right, |
234 | _resolvedPadding!.top + _resolvedPadding!.bottom, |
235 | )); |
236 | return; |
237 | } |
238 | final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding!); |
239 | child!.layout(innerConstraints, parentUsesSize: true); |
240 | final BoxParentData childParentData = child!.parentData! as BoxParentData; |
241 | childParentData.offset = Offset(_resolvedPadding!.left, _resolvedPadding!.top); |
242 | size = constraints.constrain(Size( |
243 | _resolvedPadding!.left + child!.size.width + _resolvedPadding!.right, |
244 | _resolvedPadding!.top + child!.size.height + _resolvedPadding!.bottom, |
245 | )); |
246 | } |
247 | |
248 | @override |
249 | void debugPaintSize(PaintingContext context, Offset offset) { |
250 | super.debugPaintSize(context, offset); |
251 | assert(() { |
252 | final Rect outerRect = offset & size; |
253 | debugPaintPadding(context.canvas, outerRect, child != null ? _resolvedPadding!.deflateRect(outerRect) : null); |
254 | return true; |
255 | }()); |
256 | } |
257 | |
258 | @override |
259 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
260 | super.debugFillProperties(properties); |
261 | properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding' , padding)); |
262 | properties.add(EnumProperty<TextDirection>('textDirection' , textDirection, defaultValue: null)); |
263 | } |
264 | } |
265 | |
266 | /// Abstract class for one-child-layout render boxes that use a |
267 | /// [AlignmentGeometry] to align their children. |
268 | abstract class RenderAligningShiftedBox extends RenderShiftedBox { |
269 | /// Initializes member variables for subclasses. |
270 | /// |
271 | /// The [textDirection] must be non-null if the [alignment] is |
272 | /// direction-sensitive. |
273 | RenderAligningShiftedBox({ |
274 | AlignmentGeometry alignment = Alignment.center, |
275 | required TextDirection? textDirection, |
276 | RenderBox? child, |
277 | }) : _alignment = alignment, |
278 | _textDirection = textDirection, |
279 | super(child); |
280 | |
281 | Alignment? _resolvedAlignment; |
282 | |
283 | void _resolve() { |
284 | if (_resolvedAlignment != null) { |
285 | return; |
286 | } |
287 | _resolvedAlignment = alignment.resolve(textDirection); |
288 | } |
289 | |
290 | void _markNeedResolution() { |
291 | _resolvedAlignment = null; |
292 | markNeedsLayout(); |
293 | } |
294 | |
295 | /// How to align the child. |
296 | /// |
297 | /// The x and y values of the alignment control the horizontal and vertical |
298 | /// alignment, respectively. An x value of -1.0 means that the left edge of |
299 | /// the child is aligned with the left edge of the parent whereas an x value |
300 | /// of 1.0 means that the right edge of the child is aligned with the right |
301 | /// edge of the parent. Other values interpolate (and extrapolate) linearly. |
302 | /// For example, a value of 0.0 means that the center of the child is aligned |
303 | /// with the center of the parent. |
304 | /// |
305 | /// If this is set to an [AlignmentDirectional] object, then |
306 | /// [textDirection] must not be null. |
307 | AlignmentGeometry get alignment => _alignment; |
308 | AlignmentGeometry _alignment; |
309 | /// Sets the alignment to a new value, and triggers a layout update. |
310 | set alignment(AlignmentGeometry value) { |
311 | if (_alignment == value) { |
312 | return; |
313 | } |
314 | _alignment = value; |
315 | _markNeedResolution(); |
316 | } |
317 | |
318 | /// The text direction with which to resolve [alignment]. |
319 | /// |
320 | /// This may be changed to null, but only after [alignment] has been changed |
321 | /// to a value that does not depend on the direction. |
322 | TextDirection? get textDirection => _textDirection; |
323 | TextDirection? _textDirection; |
324 | set textDirection(TextDirection? value) { |
325 | if (_textDirection == value) { |
326 | return; |
327 | } |
328 | _textDirection = value; |
329 | _markNeedResolution(); |
330 | } |
331 | |
332 | /// Apply the current [alignment] to the [child]. |
333 | /// |
334 | /// Subclasses should call this method if they have a child, to have |
335 | /// this class perform the actual alignment. If there is no child, |
336 | /// do not call this method. |
337 | /// |
338 | /// This method must be called after the child has been laid out and |
339 | /// this object's own size has been set. |
340 | @protected |
341 | void alignChild() { |
342 | _resolve(); |
343 | assert(child != null); |
344 | assert(!child!.debugNeedsLayout); |
345 | assert(child!.hasSize); |
346 | assert(hasSize); |
347 | assert(_resolvedAlignment != null); |
348 | final BoxParentData childParentData = child!.parentData! as BoxParentData; |
349 | childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset); |
350 | } |
351 | |
352 | @override |
353 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
354 | super.debugFillProperties(properties); |
355 | properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment' , alignment)); |
356 | properties.add(EnumProperty<TextDirection>('textDirection' , textDirection, defaultValue: null)); |
357 | } |
358 | } |
359 | |
360 | /// Positions its child using an [AlignmentGeometry]. |
361 | /// |
362 | /// For example, to align a box at the bottom right, you would pass this box a |
363 | /// tight constraint that is bigger than the child's natural size, |
364 | /// with an alignment of [Alignment.bottomRight]. |
365 | /// |
366 | /// By default, sizes to be as big as possible in both axes. If either axis is |
367 | /// unconstrained, then in that direction it will be sized to fit the child's |
368 | /// dimensions. Using widthFactor and heightFactor you can force this latter |
369 | /// behavior in all cases. |
370 | class RenderPositionedBox extends RenderAligningShiftedBox { |
371 | /// Creates a render object that positions its child. |
372 | RenderPositionedBox({ |
373 | super.child, |
374 | double? widthFactor, |
375 | double? heightFactor, |
376 | super.alignment, |
377 | super.textDirection, |
378 | }) : assert(widthFactor == null || widthFactor >= 0.0), |
379 | assert(heightFactor == null || heightFactor >= 0.0), |
380 | _widthFactor = widthFactor, |
381 | _heightFactor = heightFactor; |
382 | |
383 | /// If non-null, sets its width to the child's width multiplied by this factor. |
384 | /// |
385 | /// Can be both greater and less than 1.0 but must be positive. |
386 | double? get widthFactor => _widthFactor; |
387 | double? _widthFactor; |
388 | set widthFactor(double? value) { |
389 | assert(value == null || value >= 0.0); |
390 | if (_widthFactor == value) { |
391 | return; |
392 | } |
393 | _widthFactor = value; |
394 | markNeedsLayout(); |
395 | } |
396 | |
397 | /// If non-null, sets its height to the child's height multiplied by this factor. |
398 | /// |
399 | /// Can be both greater and less than 1.0 but must be positive. |
400 | double? get heightFactor => _heightFactor; |
401 | double? _heightFactor; |
402 | set heightFactor(double? value) { |
403 | assert(value == null || value >= 0.0); |
404 | if (_heightFactor == value) { |
405 | return; |
406 | } |
407 | _heightFactor = value; |
408 | markNeedsLayout(); |
409 | } |
410 | |
411 | @override |
412 | double computeMinIntrinsicWidth(double height) { |
413 | return super.computeMinIntrinsicWidth(height) * (_widthFactor ?? 1); |
414 | } |
415 | |
416 | @override |
417 | double computeMaxIntrinsicWidth(double height) { |
418 | return super.computeMaxIntrinsicWidth(height) * (_widthFactor ?? 1); |
419 | } |
420 | |
421 | @override |
422 | double computeMinIntrinsicHeight(double width) { |
423 | return super.computeMinIntrinsicHeight(width) * (_heightFactor ?? 1); |
424 | } |
425 | |
426 | @override |
427 | double computeMaxIntrinsicHeight(double width) { |
428 | return super.computeMaxIntrinsicHeight(width) * (_heightFactor ?? 1); |
429 | } |
430 | |
431 | @override |
432 | @protected |
433 | Size computeDryLayout(covariant BoxConstraints constraints) { |
434 | final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity; |
435 | final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity; |
436 | if (child != null) { |
437 | final Size childSize = child!.getDryLayout(constraints.loosen()); |
438 | return constraints.constrain(Size( |
439 | shrinkWrapWidth ? childSize.width * (_widthFactor ?? 1.0) : double.infinity, |
440 | shrinkWrapHeight ? childSize.height * (_heightFactor ?? 1.0) : double.infinity, |
441 | )); |
442 | } |
443 | return constraints.constrain(Size( |
444 | shrinkWrapWidth ? 0.0 : double.infinity, |
445 | shrinkWrapHeight ? 0.0 : double.infinity, |
446 | )); |
447 | } |
448 | |
449 | @override |
450 | void performLayout() { |
451 | final BoxConstraints constraints = this.constraints; |
452 | final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity; |
453 | final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity; |
454 | |
455 | if (child != null) { |
456 | child!.layout(constraints.loosen(), parentUsesSize: true); |
457 | size = constraints.constrain(Size( |
458 | shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity, |
459 | shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity, |
460 | )); |
461 | alignChild(); |
462 | } else { |
463 | size = constraints.constrain(Size( |
464 | shrinkWrapWidth ? 0.0 : double.infinity, |
465 | shrinkWrapHeight ? 0.0 : double.infinity, |
466 | )); |
467 | } |
468 | } |
469 | |
470 | @override |
471 | void debugPaintSize(PaintingContext context, Offset offset) { |
472 | super.debugPaintSize(context, offset); |
473 | assert(() { |
474 | final Paint paint; |
475 | if (child != null && !child!.size.isEmpty) { |
476 | final Path path; |
477 | paint = Paint() |
478 | ..style = PaintingStyle.stroke |
479 | ..strokeWidth = 1.0 |
480 | ..color = const Color(0xFFFFFF00); |
481 | path = Path(); |
482 | final BoxParentData childParentData = child!.parentData! as BoxParentData; |
483 | if (childParentData.offset.dy > 0.0) { |
484 | // vertical alignment arrows |
485 | final double headSize = math.min(childParentData.offset.dy * 0.2, 10.0); |
486 | path |
487 | ..moveTo(offset.dx + size.width / 2.0, offset.dy) |
488 | ..relativeLineTo(0.0, childParentData.offset.dy - headSize) |
489 | ..relativeLineTo(headSize, 0.0) |
490 | ..relativeLineTo(-headSize, headSize) |
491 | ..relativeLineTo(-headSize, -headSize) |
492 | ..relativeLineTo(headSize, 0.0) |
493 | ..moveTo(offset.dx + size.width / 2.0, offset.dy + size.height) |
494 | ..relativeLineTo(0.0, -childParentData.offset.dy + headSize) |
495 | ..relativeLineTo(headSize, 0.0) |
496 | ..relativeLineTo(-headSize, -headSize) |
497 | ..relativeLineTo(-headSize, headSize) |
498 | ..relativeLineTo(headSize, 0.0); |
499 | context.canvas.drawPath(path, paint); |
500 | } |
501 | if (childParentData.offset.dx > 0.0) { |
502 | // horizontal alignment arrows |
503 | final double headSize = math.min(childParentData.offset.dx * 0.2, 10.0); |
504 | path |
505 | ..moveTo(offset.dx, offset.dy + size.height / 2.0) |
506 | ..relativeLineTo(childParentData.offset.dx - headSize, 0.0) |
507 | ..relativeLineTo(0.0, headSize) |
508 | ..relativeLineTo(headSize, -headSize) |
509 | ..relativeLineTo(-headSize, -headSize) |
510 | ..relativeLineTo(0.0, headSize) |
511 | ..moveTo(offset.dx + size.width, offset.dy + size.height / 2.0) |
512 | ..relativeLineTo(-childParentData.offset.dx + headSize, 0.0) |
513 | ..relativeLineTo(0.0, headSize) |
514 | ..relativeLineTo(-headSize, -headSize) |
515 | ..relativeLineTo(headSize, -headSize) |
516 | ..relativeLineTo(0.0, headSize); |
517 | context.canvas.drawPath(path, paint); |
518 | } |
519 | } else { |
520 | paint = Paint() |
521 | ..color = const Color(0x90909090); |
522 | context.canvas.drawRect(offset & size, paint); |
523 | } |
524 | return true; |
525 | }()); |
526 | } |
527 | |
528 | @override |
529 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
530 | super.debugFillProperties(properties); |
531 | properties.add(DoubleProperty('widthFactor' , _widthFactor, ifNull: 'expand' )); |
532 | properties.add(DoubleProperty('heightFactor' , _heightFactor, ifNull: 'expand' )); |
533 | } |
534 | } |
535 | |
536 | /// How much space should be occupied by the [OverflowBox] if there is no |
537 | /// overflow. |
538 | enum OverflowBoxFit { |
539 | /// The widget will size itself to be as large as the parent allows. |
540 | max, |
541 | |
542 | /// The widget will follow the child's size. |
543 | /// |
544 | /// More specifically, the render object will size itself to match the size of |
545 | /// its child within the constraints of its parent, or as small as the |
546 | /// parent allows if no child is set. |
547 | deferToChild, |
548 | } |
549 | |
550 | /// A render object that imposes different constraints on its child than it gets |
551 | /// from its parent, possibly allowing the child to overflow the parent. |
552 | /// |
553 | /// A render overflow box proxies most functions in the render box protocol to |
554 | /// its child, except that when laying out its child, it passes constraints |
555 | /// based on the minWidth, maxWidth, minHeight, and maxHeight fields instead of |
556 | /// just passing the parent's constraints in. Specifically, it overrides any of |
557 | /// the equivalent fields on the constraints given by the parent with the |
558 | /// constraints given by these fields for each such field that is not null. It |
559 | /// then sizes itself based on the parent's constraints' maxWidth and maxHeight, |
560 | /// ignoring the child's dimensions. |
561 | /// |
562 | /// For example, if you wanted a box to always render 50 pixels high, regardless |
563 | /// of where it was rendered, you would wrap it in a |
564 | /// RenderConstrainedOverflowBox with minHeight and maxHeight set to 50.0. |
565 | /// Generally speaking, to avoid confusing behavior around hit testing, a |
566 | /// RenderConstrainedOverflowBox should usually be wrapped in a RenderClipRect. |
567 | /// |
568 | /// The child is positioned according to [alignment]. To position a smaller |
569 | /// child inside a larger parent, use [RenderPositionedBox] and |
570 | /// [RenderConstrainedBox] rather than RenderConstrainedOverflowBox. |
571 | /// |
572 | /// See also: |
573 | /// |
574 | /// * [RenderConstraintsTransformBox] for a render object that applies an |
575 | /// arbitrary transform to its constraints before sizing its child using |
576 | /// the new constraints, treating any overflow as error. |
577 | /// * [RenderSizedOverflowBox], a render object that is a specific size but |
578 | /// passes its original constraints through to its child, which it allows to |
579 | /// overflow. |
580 | class RenderConstrainedOverflowBox extends RenderAligningShiftedBox { |
581 | /// Creates a render object that lets its child overflow itself. |
582 | RenderConstrainedOverflowBox({ |
583 | super.child, |
584 | double? minWidth, |
585 | double? maxWidth, |
586 | double? minHeight, |
587 | double? maxHeight, |
588 | OverflowBoxFit fit = OverflowBoxFit.max, |
589 | super.alignment, |
590 | super.textDirection, |
591 | }) : _minWidth = minWidth, |
592 | _maxWidth = maxWidth, |
593 | _minHeight = minHeight, |
594 | _maxHeight = maxHeight, |
595 | _fit = fit; |
596 | |
597 | /// The minimum width constraint to give the child. Set this to null (the |
598 | /// default) to use the constraint from the parent instead. |
599 | double? get minWidth => _minWidth; |
600 | double? _minWidth; |
601 | set minWidth(double? value) { |
602 | if (_minWidth == value) { |
603 | return; |
604 | } |
605 | _minWidth = value; |
606 | markNeedsLayout(); |
607 | } |
608 | |
609 | /// The maximum width constraint to give the child. Set this to null (the |
610 | /// default) to use the constraint from the parent instead. |
611 | double? get maxWidth => _maxWidth; |
612 | double? _maxWidth; |
613 | set maxWidth(double? value) { |
614 | if (_maxWidth == value) { |
615 | return; |
616 | } |
617 | _maxWidth = value; |
618 | markNeedsLayout(); |
619 | } |
620 | |
621 | /// The minimum height constraint to give the child. Set this to null (the |
622 | /// default) to use the constraint from the parent instead. |
623 | double? get minHeight => _minHeight; |
624 | double? _minHeight; |
625 | set minHeight(double? value) { |
626 | if (_minHeight == value) { |
627 | return; |
628 | } |
629 | _minHeight = value; |
630 | markNeedsLayout(); |
631 | } |
632 | |
633 | /// The maximum height constraint to give the child. Set this to null (the |
634 | /// default) to use the constraint from the parent instead. |
635 | double? get maxHeight => _maxHeight; |
636 | double? _maxHeight; |
637 | set maxHeight(double? value) { |
638 | if (_maxHeight == value) { |
639 | return; |
640 | } |
641 | _maxHeight = value; |
642 | markNeedsLayout(); |
643 | } |
644 | |
645 | /// The way to size the render object. |
646 | /// |
647 | /// This only affects scenario when the child does not indeed overflow. |
648 | /// If set to [OverflowBoxFit.deferToChild], the render object will size |
649 | /// itself to match the size of its child within the constraints of its |
650 | /// parent, or as small as the parent allows if no child is set. |
651 | /// If set to [OverflowBoxFit.max] (the default), the |
652 | /// render object will size itself to be as large as the parent allows. |
653 | OverflowBoxFit get fit => _fit; |
654 | OverflowBoxFit _fit; |
655 | set fit(OverflowBoxFit value) { |
656 | if (_fit == value) { |
657 | return; |
658 | } |
659 | _fit = value; |
660 | markNeedsLayoutForSizedByParentChange(); |
661 | } |
662 | |
663 | BoxConstraints _getInnerConstraints(BoxConstraints constraints) { |
664 | return BoxConstraints( |
665 | minWidth: _minWidth ?? constraints.minWidth, |
666 | maxWidth: _maxWidth ?? constraints.maxWidth, |
667 | minHeight: _minHeight ?? constraints.minHeight, |
668 | maxHeight: _maxHeight ?? constraints.maxHeight, |
669 | ); |
670 | } |
671 | |
672 | @override |
673 | bool get sizedByParent { |
674 | switch (fit) { |
675 | case OverflowBoxFit.max: |
676 | return true; |
677 | case OverflowBoxFit.deferToChild: |
678 | // If deferToChild, the size will be as small as its child when non-overflowing, |
679 | // thus it cannot be sizedByParent. |
680 | return false; |
681 | } |
682 | } |
683 | |
684 | @override |
685 | @protected |
686 | Size computeDryLayout(covariant BoxConstraints constraints) { |
687 | switch (fit) { |
688 | case OverflowBoxFit.max: |
689 | return constraints.biggest; |
690 | case OverflowBoxFit.deferToChild: |
691 | return child?.getDryLayout(constraints) ?? constraints.smallest; |
692 | } |
693 | } |
694 | |
695 | @override |
696 | void performLayout() { |
697 | if (child != null) { |
698 | child!.layout(_getInnerConstraints(constraints), parentUsesSize: true); |
699 | switch (fit) { |
700 | case OverflowBoxFit.max: |
701 | assert(sizedByParent); |
702 | case OverflowBoxFit.deferToChild: |
703 | size = constraints.constrain(child!.size); |
704 | } |
705 | alignChild(); |
706 | } else { |
707 | switch (fit) { |
708 | case OverflowBoxFit.max: |
709 | assert(sizedByParent); |
710 | case OverflowBoxFit.deferToChild: |
711 | size = constraints.smallest; |
712 | } |
713 | } |
714 | } |
715 | |
716 | @override |
717 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
718 | super.debugFillProperties(properties); |
719 | properties.add(DoubleProperty('minWidth' , minWidth, ifNull: 'use parent minWidth constraint' )); |
720 | properties.add(DoubleProperty('maxWidth' , maxWidth, ifNull: 'use parent maxWidth constraint' )); |
721 | properties.add(DoubleProperty('minHeight' , minHeight, ifNull: 'use parent minHeight constraint' )); |
722 | properties.add(DoubleProperty('maxHeight' , maxHeight, ifNull: 'use parent maxHeight constraint' )); |
723 | properties.add(EnumProperty<OverflowBoxFit>('fit' , fit)); |
724 | } |
725 | } |
726 | |
727 | /// A [RenderBox] that applies an arbitrary transform to its constraints, |
728 | /// and sizes its child using the resulting [BoxConstraints], optionally |
729 | /// clipping, or treating the overflow as an error. |
730 | /// |
731 | /// This [RenderBox] sizes its child using a [BoxConstraints] created by |
732 | /// applying [constraintsTransform] to this [RenderBox]'s own [constraints]. |
733 | /// This box will then attempt to adopt the same size, within the limits of its |
734 | /// own constraints. If it ends up with a different size, it will align the |
735 | /// child based on [alignment]. If the box cannot expand enough to accommodate |
736 | /// the entire child, the child will be clipped if [clipBehavior] is not |
737 | /// [Clip.none]. |
738 | /// |
739 | /// In debug mode, if [clipBehavior] is [Clip.none] and the child overflows the |
740 | /// container, a warning will be printed on the console, and black and yellow |
741 | /// striped areas will appear where the overflow occurs. |
742 | /// |
743 | /// When [child] is null, this [RenderBox] takes the smallest possible size and |
744 | /// never overflows. |
745 | /// |
746 | /// This [RenderBox] can be used to ensure some of [child]'s natural dimensions |
747 | /// are honored, and get an early warning during development otherwise. For |
748 | /// instance, if [child] requires a minimum height to fully display its content, |
749 | /// [constraintsTransform] can be set to a function that removes the `maxHeight` |
750 | /// constraint from the incoming [BoxConstraints], so that if the parent |
751 | /// [RenderObject] fails to provide enough vertical space, a warning will be |
752 | /// displayed in debug mode, while still allowing [child] to grow vertically. |
753 | /// |
754 | /// See also: |
755 | /// |
756 | /// * [ConstraintsTransformBox], the widget that makes use of this |
757 | /// [RenderObject] and exposes the same functionality. |
758 | /// * [RenderConstrainedBox], which renders a box which imposes constraints |
759 | /// on its child. |
760 | /// * [RenderConstrainedOverflowBox], which renders a box that imposes different |
761 | /// constraints on its child than it gets from its parent, possibly allowing |
762 | /// the child to overflow the parent. |
763 | /// * [RenderConstraintsTransformBox] for a render object that applies an |
764 | /// arbitrary transform to its constraints before sizing its child using |
765 | /// the new constraints, treating any overflow as error. |
766 | class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugOverflowIndicatorMixin { |
767 | /// Creates a [RenderBox] that sizes itself to the child and modifies the |
768 | /// [constraints] before passing it down to that child. |
769 | RenderConstraintsTransformBox({ |
770 | required super.alignment, |
771 | required super.textDirection, |
772 | required BoxConstraintsTransform constraintsTransform, |
773 | super.child, |
774 | Clip clipBehavior = Clip.none, |
775 | }) : _constraintsTransform = constraintsTransform, |
776 | _clipBehavior = clipBehavior; |
777 | |
778 | /// {@macro flutter.widgets.constraintsTransform} |
779 | BoxConstraintsTransform get constraintsTransform => _constraintsTransform; |
780 | BoxConstraintsTransform _constraintsTransform; |
781 | set constraintsTransform(BoxConstraintsTransform value) { |
782 | if (_constraintsTransform == value) { |
783 | return; |
784 | } |
785 | _constraintsTransform = value; |
786 | // The RenderObject only needs layout if the new transform maps the current |
787 | // `constraints` to a different value, or the render object has never been |
788 | // laid out before. |
789 | final bool needsLayout = _childConstraints == null |
790 | || _childConstraints != value(constraints); |
791 | if (needsLayout) { |
792 | markNeedsLayout(); |
793 | } |
794 | } |
795 | |
796 | /// {@macro flutter.material.Material.clipBehavior} |
797 | /// |
798 | /// {@macro flutter.widgets.ConstraintsTransformBox.clipBehavior} |
799 | /// |
800 | /// Defaults to [Clip.none]. |
801 | Clip get clipBehavior => _clipBehavior; |
802 | Clip _clipBehavior; |
803 | set clipBehavior(Clip value) { |
804 | if (value != _clipBehavior) { |
805 | _clipBehavior = value; |
806 | markNeedsPaint(); |
807 | markNeedsSemanticsUpdate(); |
808 | } |
809 | } |
810 | |
811 | @override |
812 | double computeMinIntrinsicHeight(double width) { |
813 | return super.computeMinIntrinsicHeight( |
814 | constraintsTransform(BoxConstraints(maxWidth: width)).maxWidth, |
815 | ); |
816 | } |
817 | |
818 | @override |
819 | double computeMaxIntrinsicHeight(double width) { |
820 | return super.computeMaxIntrinsicHeight( |
821 | constraintsTransform(BoxConstraints(maxWidth: width)).maxWidth, |
822 | ); |
823 | } |
824 | |
825 | @override |
826 | double computeMinIntrinsicWidth(double height) { |
827 | return super.computeMinIntrinsicWidth( |
828 | constraintsTransform(BoxConstraints(maxHeight: height)).maxHeight, |
829 | ); |
830 | } |
831 | |
832 | @override |
833 | double computeMaxIntrinsicWidth(double height) { |
834 | return super.computeMaxIntrinsicWidth( |
835 | constraintsTransform(BoxConstraints(maxHeight: height)).maxHeight, |
836 | ); |
837 | } |
838 | |
839 | @override |
840 | @protected |
841 | Size computeDryLayout(covariant BoxConstraints constraints) { |
842 | final Size? childSize = child?.getDryLayout(constraintsTransform(constraints)); |
843 | return childSize == null ? constraints.smallest : constraints.constrain(childSize); |
844 | } |
845 | |
846 | Rect _overflowContainerRect = Rect.zero; |
847 | Rect _overflowChildRect = Rect.zero; |
848 | bool _isOverflowing = false; |
849 | |
850 | BoxConstraints? _childConstraints; |
851 | |
852 | @override |
853 | void performLayout() { |
854 | final BoxConstraints constraints = this.constraints; |
855 | final RenderBox? child = this.child; |
856 | if (child != null) { |
857 | final BoxConstraints childConstraints = constraintsTransform(constraints); |
858 | assert(childConstraints.isNormalized, ' $childConstraints is not normalized' ); |
859 | _childConstraints = childConstraints; |
860 | child.layout(childConstraints, parentUsesSize: true); |
861 | size = constraints.constrain(child.size); |
862 | alignChild(); |
863 | final BoxParentData childParentData = child.parentData! as BoxParentData; |
864 | _overflowContainerRect = Offset.zero & size; |
865 | _overflowChildRect = childParentData.offset & child.size; |
866 | } else { |
867 | size = constraints.smallest; |
868 | _overflowContainerRect = Rect.zero; |
869 | _overflowChildRect = Rect.zero; |
870 | } |
871 | _isOverflowing = RelativeRect.fromRect(_overflowContainerRect, _overflowChildRect).hasInsets; |
872 | } |
873 | |
874 | @override |
875 | void paint(PaintingContext context, Offset offset) { |
876 | // There's no point in drawing the child if we're empty, or there is no |
877 | // child. |
878 | if (child == null || size.isEmpty) { |
879 | return; |
880 | } |
881 | |
882 | if (!_isOverflowing) { |
883 | super.paint(context, offset); |
884 | return; |
885 | } |
886 | |
887 | // We have overflow and the clipBehavior isn't none. Clip it. |
888 | _clipRectLayer.layer = context.pushClipRect( |
889 | needsCompositing, |
890 | offset, |
891 | Offset.zero & size, |
892 | super.paint, |
893 | clipBehavior: clipBehavior, |
894 | oldLayer: _clipRectLayer.layer, |
895 | ); |
896 | |
897 | // Display the overflow indicator if clipBehavior is Clip.none. |
898 | assert(() { |
899 | switch (clipBehavior) { |
900 | case Clip.none: |
901 | paintOverflowIndicator(context, offset, _overflowContainerRect, _overflowChildRect); |
902 | case Clip.hardEdge: |
903 | case Clip.antiAlias: |
904 | case Clip.antiAliasWithSaveLayer: |
905 | break; |
906 | } |
907 | return true; |
908 | }()); |
909 | } |
910 | |
911 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
912 | |
913 | @override |
914 | void dispose() { |
915 | _clipRectLayer.layer = null; |
916 | super.dispose(); |
917 | } |
918 | |
919 | @override |
920 | Rect? describeApproximatePaintClip(RenderObject child) { |
921 | switch (clipBehavior) { |
922 | case Clip.none: |
923 | return null; |
924 | case Clip.hardEdge: |
925 | case Clip.antiAlias: |
926 | case Clip.antiAliasWithSaveLayer: |
927 | return _isOverflowing ? Offset.zero & size : null; |
928 | } |
929 | } |
930 | |
931 | @override |
932 | String toStringShort() { |
933 | String header = super.toStringShort(); |
934 | if (!kReleaseMode) { |
935 | if (_isOverflowing) { |
936 | header += ' OVERFLOWING' ; |
937 | } |
938 | } |
939 | return header; |
940 | } |
941 | } |
942 | |
943 | /// A render object that is a specific size but passes its original constraints |
944 | /// through to its child, which it allows to overflow. |
945 | /// |
946 | /// If the child's resulting size differs from this render object's size, then |
947 | /// the child is aligned according to the [alignment] property. |
948 | /// |
949 | /// See also: |
950 | /// |
951 | /// * [RenderConstraintsTransformBox] for a render object that applies an |
952 | /// arbitrary transform to its constraints before sizing its child using |
953 | /// the new constraints, treating any overflow as error. |
954 | /// * [RenderConstrainedOverflowBox] for a render object that imposes |
955 | /// different constraints on its child than it gets from its parent, |
956 | /// possibly allowing the child to overflow the parent. |
957 | class RenderSizedOverflowBox extends RenderAligningShiftedBox { |
958 | /// Creates a render box of a given size that lets its child overflow. |
959 | /// |
960 | /// The [textDirection] argument must not be null if the [alignment] is |
961 | /// direction-sensitive. |
962 | RenderSizedOverflowBox({ |
963 | super.child, |
964 | required Size requestedSize, |
965 | super.alignment, |
966 | super.textDirection, |
967 | }) : _requestedSize = requestedSize; |
968 | |
969 | /// The size this render box should attempt to be. |
970 | Size get requestedSize => _requestedSize; |
971 | Size _requestedSize; |
972 | set requestedSize(Size value) { |
973 | if (_requestedSize == value) { |
974 | return; |
975 | } |
976 | _requestedSize = value; |
977 | markNeedsLayout(); |
978 | } |
979 | |
980 | @override |
981 | double computeMinIntrinsicWidth(double height) { |
982 | return _requestedSize.width; |
983 | } |
984 | |
985 | @override |
986 | double computeMaxIntrinsicWidth(double height) { |
987 | return _requestedSize.width; |
988 | } |
989 | |
990 | @override |
991 | double computeMinIntrinsicHeight(double width) { |
992 | return _requestedSize.height; |
993 | } |
994 | |
995 | @override |
996 | double computeMaxIntrinsicHeight(double width) { |
997 | return _requestedSize.height; |
998 | } |
999 | |
1000 | @override |
1001 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
1002 | if (child != null) { |
1003 | return child!.getDistanceToActualBaseline(baseline); |
1004 | } |
1005 | return super.computeDistanceToActualBaseline(baseline); |
1006 | } |
1007 | |
1008 | @override |
1009 | @protected |
1010 | Size computeDryLayout(covariant BoxConstraints constraints) { |
1011 | return constraints.constrain(_requestedSize); |
1012 | } |
1013 | |
1014 | @override |
1015 | void performLayout() { |
1016 | size = constraints.constrain(_requestedSize); |
1017 | if (child != null) { |
1018 | child!.layout(constraints, parentUsesSize: true); |
1019 | alignChild(); |
1020 | } |
1021 | } |
1022 | } |
1023 | |
1024 | /// Sizes its child to a fraction of the total available space. |
1025 | /// |
1026 | /// For both its width and height, this render object imposes a tight |
1027 | /// constraint on its child that is a multiple (typically less than 1.0) of the |
1028 | /// maximum constraint it received from its parent on that axis. If the factor |
1029 | /// for a given axis is null, then the constraints from the parent are just |
1030 | /// passed through instead. |
1031 | /// |
1032 | /// It then tries to size itself to the size of its child. Where this is not |
1033 | /// possible (e.g. if the constraints from the parent are themselves tight), the |
1034 | /// child is aligned according to [alignment]. |
1035 | class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox { |
1036 | /// Creates a render box that sizes its child to a fraction of the total available space. |
1037 | /// |
1038 | /// If non-null, the [widthFactor] and [heightFactor] arguments must be |
1039 | /// non-negative. |
1040 | /// |
1041 | /// The [textDirection] must be non-null if the [alignment] is |
1042 | /// direction-sensitive. |
1043 | RenderFractionallySizedOverflowBox({ |
1044 | super.child, |
1045 | double? widthFactor, |
1046 | double? heightFactor, |
1047 | super.alignment, |
1048 | super.textDirection, |
1049 | }) : _widthFactor = widthFactor, |
1050 | _heightFactor = heightFactor { |
1051 | assert(_widthFactor == null || _widthFactor! >= 0.0); |
1052 | assert(_heightFactor == null || _heightFactor! >= 0.0); |
1053 | } |
1054 | |
1055 | /// If non-null, the factor of the incoming width to use. |
1056 | /// |
1057 | /// If non-null, the child is given a tight width constraint that is the max |
1058 | /// incoming width constraint multiplied by this factor. If null, the child is |
1059 | /// given the incoming width constraints. |
1060 | double? get widthFactor => _widthFactor; |
1061 | double? _widthFactor; |
1062 | set widthFactor(double? value) { |
1063 | assert(value == null || value >= 0.0); |
1064 | if (_widthFactor == value) { |
1065 | return; |
1066 | } |
1067 | _widthFactor = value; |
1068 | markNeedsLayout(); |
1069 | } |
1070 | |
1071 | /// If non-null, the factor of the incoming height to use. |
1072 | /// |
1073 | /// If non-null, the child is given a tight height constraint that is the max |
1074 | /// incoming width constraint multiplied by this factor. If null, the child is |
1075 | /// given the incoming width constraints. |
1076 | double? get heightFactor => _heightFactor; |
1077 | double? _heightFactor; |
1078 | set heightFactor(double? value) { |
1079 | assert(value == null || value >= 0.0); |
1080 | if (_heightFactor == value) { |
1081 | return; |
1082 | } |
1083 | _heightFactor = value; |
1084 | markNeedsLayout(); |
1085 | } |
1086 | |
1087 | BoxConstraints _getInnerConstraints(BoxConstraints constraints) { |
1088 | double minWidth = constraints.minWidth; |
1089 | double maxWidth = constraints.maxWidth; |
1090 | if (_widthFactor != null) { |
1091 | final double width = maxWidth * _widthFactor!; |
1092 | minWidth = width; |
1093 | maxWidth = width; |
1094 | } |
1095 | double minHeight = constraints.minHeight; |
1096 | double maxHeight = constraints.maxHeight; |
1097 | if (_heightFactor != null) { |
1098 | final double height = maxHeight * _heightFactor!; |
1099 | minHeight = height; |
1100 | maxHeight = height; |
1101 | } |
1102 | return BoxConstraints( |
1103 | minWidth: minWidth, |
1104 | maxWidth: maxWidth, |
1105 | minHeight: minHeight, |
1106 | maxHeight: maxHeight, |
1107 | ); |
1108 | } |
1109 | |
1110 | @override |
1111 | double computeMinIntrinsicWidth(double height) { |
1112 | final double result; |
1113 | if (child == null) { |
1114 | result = super.computeMinIntrinsicWidth(height); |
1115 | } else { // the following line relies on double.infinity absorption |
1116 | result = child!.getMinIntrinsicWidth(height * (_heightFactor ?? 1.0)); |
1117 | } |
1118 | assert(result.isFinite); |
1119 | return result / (_widthFactor ?? 1.0); |
1120 | } |
1121 | |
1122 | @override |
1123 | double computeMaxIntrinsicWidth(double height) { |
1124 | final double result; |
1125 | if (child == null) { |
1126 | result = super.computeMaxIntrinsicWidth(height); |
1127 | } else { // the following line relies on double.infinity absorption |
1128 | result = child!.getMaxIntrinsicWidth(height * (_heightFactor ?? 1.0)); |
1129 | } |
1130 | assert(result.isFinite); |
1131 | return result / (_widthFactor ?? 1.0); |
1132 | } |
1133 | |
1134 | @override |
1135 | double computeMinIntrinsicHeight(double width) { |
1136 | final double result; |
1137 | if (child == null) { |
1138 | result = super.computeMinIntrinsicHeight(width); |
1139 | } else { // the following line relies on double.infinity absorption |
1140 | result = child!.getMinIntrinsicHeight(width * (_widthFactor ?? 1.0)); |
1141 | } |
1142 | assert(result.isFinite); |
1143 | return result / (_heightFactor ?? 1.0); |
1144 | } |
1145 | |
1146 | @override |
1147 | double computeMaxIntrinsicHeight(double width) { |
1148 | final double result; |
1149 | if (child == null) { |
1150 | result = super.computeMaxIntrinsicHeight(width); |
1151 | } else { // the following line relies on double.infinity absorption |
1152 | result = child!.getMaxIntrinsicHeight(width * (_widthFactor ?? 1.0)); |
1153 | } |
1154 | assert(result.isFinite); |
1155 | return result / (_heightFactor ?? 1.0); |
1156 | } |
1157 | |
1158 | @override |
1159 | @protected |
1160 | Size computeDryLayout(covariant BoxConstraints constraints) { |
1161 | if (child != null) { |
1162 | final Size childSize = child!.getDryLayout(_getInnerConstraints(constraints)); |
1163 | return constraints.constrain(childSize); |
1164 | } |
1165 | return constraints.constrain(_getInnerConstraints(constraints).constrain(Size.zero)); |
1166 | } |
1167 | |
1168 | @override |
1169 | void performLayout() { |
1170 | if (child != null) { |
1171 | child!.layout(_getInnerConstraints(constraints), parentUsesSize: true); |
1172 | size = constraints.constrain(child!.size); |
1173 | alignChild(); |
1174 | } else { |
1175 | size = constraints.constrain(_getInnerConstraints(constraints).constrain(Size.zero)); |
1176 | } |
1177 | } |
1178 | |
1179 | @override |
1180 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
1181 | super.debugFillProperties(properties); |
1182 | properties.add(DoubleProperty('widthFactor' , _widthFactor, ifNull: 'pass-through' )); |
1183 | properties.add(DoubleProperty('heightFactor' , _heightFactor, ifNull: 'pass-through' )); |
1184 | } |
1185 | } |
1186 | |
1187 | /// A delegate for computing the layout of a render object with a single child. |
1188 | /// |
1189 | /// Used by [CustomSingleChildLayout] (in the widgets library) and |
1190 | /// [RenderCustomSingleChildLayoutBox] (in the rendering library). |
1191 | /// |
1192 | /// When asked to layout, [CustomSingleChildLayout] first calls [getSize] with |
1193 | /// its incoming constraints to determine its size. It then calls |
1194 | /// [getConstraintsForChild] to determine the constraints to apply to the child. |
1195 | /// After the child completes its layout, [RenderCustomSingleChildLayoutBox] |
1196 | /// calls [getPositionForChild] to determine the child's position. |
1197 | /// |
1198 | /// The [shouldRelayout] method is called when a new instance of the class |
1199 | /// is provided, to check if the new instance actually represents different |
1200 | /// information. |
1201 | /// |
1202 | /// The most efficient way to trigger a relayout is to supply a `relayout` |
1203 | /// argument to the constructor of the [SingleChildLayoutDelegate]. The custom |
1204 | /// layout will listen to this value and relayout whenever the Listenable |
1205 | /// notifies its listeners, such as when an [Animation] ticks. This allows |
1206 | /// the custom layout to avoid the build phase of the pipeline. |
1207 | /// |
1208 | /// See also: |
1209 | /// |
1210 | /// * [CustomSingleChildLayout], the widget that uses this delegate. |
1211 | /// * [RenderCustomSingleChildLayoutBox], render object that uses this |
1212 | /// delegate. |
1213 | abstract class SingleChildLayoutDelegate { |
1214 | /// Creates a layout delegate. |
1215 | /// |
1216 | /// The layout will update whenever [relayout] notifies its listeners. |
1217 | const SingleChildLayoutDelegate({ Listenable? relayout }) : _relayout = relayout; |
1218 | |
1219 | final Listenable? _relayout; |
1220 | |
1221 | /// The size of this object given the incoming constraints. |
1222 | /// |
1223 | /// Defaults to the biggest size that satisfies the given constraints. |
1224 | Size getSize(BoxConstraints constraints) => constraints.biggest; |
1225 | |
1226 | /// The constraints for the child given the incoming constraints. |
1227 | /// |
1228 | /// During layout, the child is given the layout constraints returned by this |
1229 | /// function. The child is required to pick a size for itself that satisfies |
1230 | /// these constraints. |
1231 | /// |
1232 | /// Defaults to the given constraints. |
1233 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints; |
1234 | |
1235 | /// The position where the child should be placed. |
1236 | /// |
1237 | /// The `size` argument is the size of the parent, which might be different |
1238 | /// from the value returned by [getSize] if that size doesn't satisfy the |
1239 | /// constraints passed to [getSize]. The `childSize` argument is the size of |
1240 | /// the child, which will satisfy the constraints returned by |
1241 | /// [getConstraintsForChild]. |
1242 | /// |
1243 | /// Defaults to positioning the child in the upper left corner of the parent. |
1244 | Offset getPositionForChild(Size size, Size childSize) => Offset.zero; |
1245 | |
1246 | /// Called whenever a new instance of the custom layout delegate class is |
1247 | /// provided to the [RenderCustomSingleChildLayoutBox] object, or any time |
1248 | /// that a new [CustomSingleChildLayout] object is created with a new instance |
1249 | /// of the custom layout delegate class (which amounts to the same thing, |
1250 | /// because the latter is implemented in terms of the former). |
1251 | /// |
1252 | /// If the new instance represents different information than the old |
1253 | /// instance, then the method should return true, otherwise it should return |
1254 | /// false. |
1255 | /// |
1256 | /// If the method returns false, then the [getSize], |
1257 | /// [getConstraintsForChild], and [getPositionForChild] calls might be |
1258 | /// optimized away. |
1259 | /// |
1260 | /// It's possible that the layout methods will get called even if |
1261 | /// [shouldRelayout] returns false (e.g. if an ancestor changed its layout). |
1262 | /// It's also possible that the layout method will get called |
1263 | /// without [shouldRelayout] being called at all (e.g. if the parent changes |
1264 | /// size). |
1265 | bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate); |
1266 | } |
1267 | |
1268 | /// Defers the layout of its single child to a delegate. |
1269 | /// |
1270 | /// The delegate can determine the layout constraints for the child and can |
1271 | /// decide where to position the child. The delegate can also determine the size |
1272 | /// of the parent, but the size of the parent cannot depend on the size of the |
1273 | /// child. |
1274 | class RenderCustomSingleChildLayoutBox extends RenderShiftedBox { |
1275 | /// Creates a render box that defers its layout to a delegate. |
1276 | /// |
1277 | /// The [delegate] argument must not be null. |
1278 | RenderCustomSingleChildLayoutBox({ |
1279 | RenderBox? child, |
1280 | required SingleChildLayoutDelegate delegate, |
1281 | }) : _delegate = delegate, |
1282 | super(child); |
1283 | |
1284 | /// A delegate that controls this object's layout. |
1285 | SingleChildLayoutDelegate get delegate => _delegate; |
1286 | SingleChildLayoutDelegate _delegate; |
1287 | set delegate(SingleChildLayoutDelegate newDelegate) { |
1288 | if (_delegate == newDelegate) { |
1289 | return; |
1290 | } |
1291 | final SingleChildLayoutDelegate oldDelegate = _delegate; |
1292 | if (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRelayout(oldDelegate)) { |
1293 | markNeedsLayout(); |
1294 | } |
1295 | _delegate = newDelegate; |
1296 | if (attached) { |
1297 | oldDelegate._relayout?.removeListener(markNeedsLayout); |
1298 | newDelegate._relayout?.addListener(markNeedsLayout); |
1299 | } |
1300 | } |
1301 | |
1302 | @override |
1303 | void attach(PipelineOwner owner) { |
1304 | super.attach(owner); |
1305 | _delegate._relayout?.addListener(markNeedsLayout); |
1306 | } |
1307 | |
1308 | @override |
1309 | void detach() { |
1310 | _delegate._relayout?.removeListener(markNeedsLayout); |
1311 | super.detach(); |
1312 | } |
1313 | |
1314 | Size _getSize(BoxConstraints constraints) { |
1315 | return constraints.constrain(_delegate.getSize(constraints)); |
1316 | } |
1317 | |
1318 | // TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to |
1319 | // figure out the intrinsic dimensions. We really should either not support intrinsics, |
1320 | // or we should expose intrinsic delegate callbacks and throw if they're not implemented. |
1321 | |
1322 | @override |
1323 | double computeMinIntrinsicWidth(double height) { |
1324 | final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width; |
1325 | if (width.isFinite) { |
1326 | return width; |
1327 | } |
1328 | return 0.0; |
1329 | } |
1330 | |
1331 | @override |
1332 | double computeMaxIntrinsicWidth(double height) { |
1333 | final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width; |
1334 | if (width.isFinite) { |
1335 | return width; |
1336 | } |
1337 | return 0.0; |
1338 | } |
1339 | |
1340 | @override |
1341 | double computeMinIntrinsicHeight(double width) { |
1342 | final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height; |
1343 | if (height.isFinite) { |
1344 | return height; |
1345 | } |
1346 | return 0.0; |
1347 | } |
1348 | |
1349 | @override |
1350 | double computeMaxIntrinsicHeight(double width) { |
1351 | final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height; |
1352 | if (height.isFinite) { |
1353 | return height; |
1354 | } |
1355 | return 0.0; |
1356 | } |
1357 | |
1358 | @override |
1359 | @protected |
1360 | Size computeDryLayout(covariant BoxConstraints constraints) { |
1361 | return _getSize(constraints); |
1362 | } |
1363 | |
1364 | @override |
1365 | void performLayout() { |
1366 | size = _getSize(constraints); |
1367 | if (child != null) { |
1368 | final BoxConstraints childConstraints = delegate.getConstraintsForChild(constraints); |
1369 | assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true)); |
1370 | child!.layout(childConstraints, parentUsesSize: !childConstraints.isTight); |
1371 | final BoxParentData childParentData = child!.parentData! as BoxParentData; |
1372 | childParentData.offset = delegate.getPositionForChild(size, childConstraints.isTight ? childConstraints.smallest : child!.size); |
1373 | } |
1374 | } |
1375 | } |
1376 | |
1377 | /// Shifts the child down such that the child's baseline (or the |
1378 | /// bottom of the child, if the child has no baseline) is [baseline] |
1379 | /// logical pixels below the top of this box, then sizes this box to |
1380 | /// contain the child. |
1381 | /// |
1382 | /// If [baseline] is less than the distance from the top of the child |
1383 | /// to the baseline of the child, then the child will overflow the top |
1384 | /// of the box. This is typically not desirable, in particular, that |
1385 | /// part of the child will not be found when doing hit tests, so the |
1386 | /// user cannot interact with that part of the child. |
1387 | /// |
1388 | /// This box will be sized so that its bottom is coincident with the |
1389 | /// bottom of the child. This means if this box shifts the child down, |
1390 | /// there will be space between the top of this box and the top of the |
1391 | /// child, but there is never space between the bottom of the child |
1392 | /// and the bottom of the box. |
1393 | class RenderBaseline extends RenderShiftedBox { |
1394 | /// Creates a [RenderBaseline] object. |
1395 | RenderBaseline({ |
1396 | RenderBox? child, |
1397 | required double baseline, |
1398 | required TextBaseline baselineType, |
1399 | }) : _baseline = baseline, |
1400 | _baselineType = baselineType, |
1401 | super(child); |
1402 | |
1403 | /// The number of logical pixels from the top of this box at which to position |
1404 | /// the child's baseline. |
1405 | double get baseline => _baseline; |
1406 | double _baseline; |
1407 | set baseline(double value) { |
1408 | if (_baseline == value) { |
1409 | return; |
1410 | } |
1411 | _baseline = value; |
1412 | markNeedsLayout(); |
1413 | } |
1414 | |
1415 | /// The type of baseline to use for positioning the child. |
1416 | TextBaseline get baselineType => _baselineType; |
1417 | TextBaseline _baselineType; |
1418 | set baselineType(TextBaseline value) { |
1419 | if (_baselineType == value) { |
1420 | return; |
1421 | } |
1422 | _baselineType = value; |
1423 | markNeedsLayout(); |
1424 | } |
1425 | |
1426 | @override |
1427 | @protected |
1428 | Size computeDryLayout(covariant BoxConstraints constraints) { |
1429 | if (child != null) { |
1430 | assert(debugCannotComputeDryLayout( |
1431 | reason: 'Baseline metrics are only available after a full layout.' , |
1432 | )); |
1433 | return Size.zero; |
1434 | } |
1435 | return constraints.smallest; |
1436 | } |
1437 | |
1438 | @override |
1439 | void performLayout() { |
1440 | if (child != null) { |
1441 | final BoxConstraints constraints = this.constraints; |
1442 | child!.layout(constraints.loosen(), parentUsesSize: true); |
1443 | final double childBaseline = child!.getDistanceToBaseline(baselineType)!; |
1444 | final double actualBaseline = baseline; |
1445 | final double top = actualBaseline - childBaseline; |
1446 | final BoxParentData childParentData = child!.parentData! as BoxParentData; |
1447 | childParentData.offset = Offset(0.0, top); |
1448 | final Size childSize = child!.size; |
1449 | size = constraints.constrain(Size(childSize.width, top + childSize.height)); |
1450 | } else { |
1451 | size = constraints.smallest; |
1452 | } |
1453 | } |
1454 | |
1455 | @override |
1456 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
1457 | super.debugFillProperties(properties); |
1458 | properties.add(DoubleProperty('baseline' , baseline)); |
1459 | properties.add(EnumProperty<TextBaseline>('baselineType' , baselineType)); |
1460 | } |
1461 | } |
1462 | |