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 | /// @docImport 'package:flutter/widgets.dart'; |
6 | /// |
7 | /// @docImport 'stack.dart'; |
8 | library; |
9 | |
10 | import 'dart:ui' as ui show Color; |
11 | |
12 | import 'package:flutter/foundation.dart'; |
13 | import 'package:vector_math/vector_math_64.dart'; |
14 | |
15 | import 'box.dart'; |
16 | import 'layer.dart'; |
17 | import 'object.dart'; |
18 | |
19 | /// A context in which a [FlowDelegate] paints. |
20 | /// |
21 | /// Provides information about the current size of the container and the |
22 | /// children and a mechanism for painting children. |
23 | /// |
24 | /// See also: |
25 | /// |
26 | /// * [FlowDelegate] |
27 | /// * [Flow] |
28 | /// * [RenderFlow] |
29 | abstract class FlowPaintingContext { |
30 | /// The size of the container in which the children can be painted. |
31 | Size get size; |
32 | |
33 | /// The number of children available to paint. |
34 | int get childCount; |
35 | |
36 | /// The size of the [i]th child. |
37 | /// |
38 | /// If [i] is negative or exceeds [childCount], returns null. |
39 | Size? getChildSize(int i); |
40 | |
41 | /// Paint the [i]th child using the given transform. |
42 | /// |
43 | /// The child will be painted in a coordinate system that concatenates the |
44 | /// container's coordinate system with the given transform. The origin of the |
45 | /// parent's coordinate system is the upper left corner of the parent, with |
46 | /// x increasing rightward and y increasing downward. |
47 | /// |
48 | /// The container will clip the children to its bounds. |
49 | void paintChild(int i, {Matrix4 transform, double opacity = 1.0}); |
50 | } |
51 | |
52 | /// A delegate that controls the appearance of a flow layout. |
53 | /// |
54 | /// Flow layouts are optimized for moving children around the screen using |
55 | /// transformation matrices. For optimal performance, construct the |
56 | /// [FlowDelegate] with an [Animation] that ticks whenever the delegate wishes |
57 | /// to change the transformation matrices for the children and avoid rebuilding |
58 | /// the [Flow] widget itself every animation frame. |
59 | /// |
60 | /// See also: |
61 | /// |
62 | /// * [Flow] |
63 | /// * [RenderFlow] |
64 | abstract class FlowDelegate { |
65 | /// The flow will repaint whenever [repaint] notifies its listeners. |
66 | const FlowDelegate({Listenable? repaint}) : _repaint = repaint; |
67 | |
68 | final Listenable? _repaint; |
69 | |
70 | /// Override to control the size of the container for the children. |
71 | /// |
72 | /// By default, the flow will be as large as possible. If this function |
73 | /// returns a size that does not respect the given constraints, the size will |
74 | /// be adjusted to be as close to the returned size as possible while still |
75 | /// respecting the constraints. |
76 | /// |
77 | /// If this function depends on information other than the given constraints, |
78 | /// override [shouldRelayout] to indicate when the container should |
79 | /// relayout. |
80 | Size getSize(BoxConstraints constraints) => constraints.biggest; |
81 | |
82 | /// Override to control the layout constraints given to each child. |
83 | /// |
84 | /// By default, the children will receive the given constraints, which are the |
85 | /// constraints used to size the container. The children need |
86 | /// not respect the given constraints, but they are required to respect the |
87 | /// returned constraints. For example, the incoming constraints might require |
88 | /// the container to have a width of exactly 100.0 and a height of exactly |
89 | /// 100.0, but this function might give the children looser constraints that |
90 | /// let them be larger or smaller than 100.0 by 100.0. |
91 | /// |
92 | /// If this function depends on information other than the given constraints, |
93 | /// override [shouldRelayout] to indicate when the container should |
94 | /// relayout. |
95 | BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) => constraints; |
96 | |
97 | /// Override to paint the children of the flow. |
98 | /// |
99 | /// Children can be painted in any order, but each child can be painted at |
100 | /// most once. Although the container clips the children to its own bounds, it |
101 | /// is more efficient to skip painting a child altogether rather than having |
102 | /// it paint entirely outside the container's clip. |
103 | /// |
104 | /// To paint a child, call [FlowPaintingContext.paintChild] on the given |
105 | /// [FlowPaintingContext] (the `context` argument). The given context is valid |
106 | /// only within the scope of this function call and contains information (such |
107 | /// as the size of the container) that is useful for picking transformation |
108 | /// matrices for the children. |
109 | /// |
110 | /// If this function depends on information other than the given context, |
111 | /// override [shouldRepaint] to indicate when the container should |
112 | /// relayout. |
113 | void paintChildren(FlowPaintingContext context); |
114 | |
115 | /// Override this method to return true when the children need to be laid out. |
116 | /// This should compare the fields of the current delegate and the given |
117 | /// oldDelegate and return true if the fields are such that the layout would |
118 | /// be different. |
119 | bool shouldRelayout(covariant FlowDelegate oldDelegate) => false; |
120 | |
121 | /// Override this method to return true when the children need to be |
122 | /// repainted. This should compare the fields of the current delegate and the |
123 | /// given oldDelegate and return true if the fields are such that |
124 | /// paintChildren would act differently. |
125 | /// |
126 | /// The delegate can also trigger a repaint if the delegate provides the |
127 | /// repaint animation argument to this object's constructor and that animation |
128 | /// ticks. Triggering a repaint using this animation-based mechanism is more |
129 | /// efficient than rebuilding the [Flow] widget to change its delegate. |
130 | /// |
131 | /// The flow container might repaint even if this function returns false, for |
132 | /// example if layout triggers painting (e.g., if [shouldRelayout] returns |
133 | /// true). |
134 | bool shouldRepaint(covariant FlowDelegate oldDelegate); |
135 | |
136 | /// Override this method to include additional information in the |
137 | /// debugging data printed by [debugDumpRenderTree] and friends. |
138 | /// |
139 | /// By default, returns the [runtimeType] of the class. |
140 | @override |
141 | String toString() => objectRuntimeType(this, 'FlowDelegate'); |
142 | } |
143 | |
144 | /// Parent data for use with [RenderFlow]. |
145 | /// |
146 | /// The [offset] property is ignored by [RenderFlow] and is always set to |
147 | /// [Offset.zero]. Children of a [RenderFlow] are positioned using a |
148 | /// transformation matrix, which is private to the [RenderFlow]. To set the |
149 | /// matrix, use the [FlowPaintingContext.paintChild] function from an override |
150 | /// of the [FlowDelegate.paintChildren] function. |
151 | class FlowParentData extends ContainerBoxParentData<RenderBox> { |
152 | Matrix4? _transform; |
153 | } |
154 | |
155 | /// Implements the flow layout algorithm. |
156 | /// |
157 | /// Flow layouts are optimized for repositioning children using transformation |
158 | /// matrices. |
159 | /// |
160 | /// The flow container is sized independently from the children by the |
161 | /// [FlowDelegate.getSize] function of the delegate. The children are then sized |
162 | /// independently given the constraints from the |
163 | /// [FlowDelegate.getConstraintsForChild] function. |
164 | /// |
165 | /// Rather than positioning the children during layout, the children are |
166 | /// positioned using transformation matrices during the paint phase using the |
167 | /// matrices from the [FlowDelegate.paintChildren] function. The children are thus |
168 | /// repositioned efficiently by repainting the flow, skipping layout. |
169 | /// |
170 | /// The most efficient way to trigger a repaint of the flow is to supply a |
171 | /// repaint argument to the constructor of the [FlowDelegate]. The flow will |
172 | /// listen to this animation and repaint whenever the animation ticks, avoiding |
173 | /// both the build and layout phases of the pipeline. |
174 | /// |
175 | /// See also: |
176 | /// |
177 | /// * [FlowDelegate] |
178 | /// * [RenderStack] |
179 | class RenderFlow extends RenderBox |
180 | with |
181 | ContainerRenderObjectMixin<RenderBox, FlowParentData>, |
182 | RenderBoxContainerDefaultsMixin<RenderBox, FlowParentData> |
183 | implements FlowPaintingContext { |
184 | /// Creates a render object for a flow layout. |
185 | /// |
186 | /// For optimal performance, consider using children that return true from |
187 | /// [isRepaintBoundary]. |
188 | RenderFlow({ |
189 | List<RenderBox>? children, |
190 | required FlowDelegate delegate, |
191 | Clip clipBehavior = Clip.hardEdge, |
192 | }) : _delegate = delegate, |
193 | _clipBehavior = clipBehavior { |
194 | addAll(children); |
195 | } |
196 | |
197 | @override |
198 | void setupParentData(RenderBox child) { |
199 | final ParentData? childParentData = child.parentData; |
200 | if (childParentData is FlowParentData) { |
201 | childParentData._transform = null; |
202 | } else { |
203 | child.parentData = FlowParentData(); |
204 | } |
205 | } |
206 | |
207 | /// The delegate that controls the transformation matrices of the children. |
208 | FlowDelegate get delegate => _delegate; |
209 | FlowDelegate _delegate; |
210 | |
211 | /// When the delegate is changed to a new delegate with the same runtimeType |
212 | /// as the old delegate, this object will call the delegate's |
213 | /// [FlowDelegate.shouldRelayout] and [FlowDelegate.shouldRepaint] functions |
214 | /// to determine whether the new delegate requires this object to update its |
215 | /// layout or painting. |
216 | set delegate(FlowDelegate newDelegate) { |
217 | if (_delegate == newDelegate) { |
218 | return; |
219 | } |
220 | final FlowDelegate oldDelegate = _delegate; |
221 | _delegate = newDelegate; |
222 | |
223 | if (newDelegate.runtimeType != oldDelegate.runtimeType || |
224 | newDelegate.shouldRelayout(oldDelegate)) { |
225 | markNeedsLayout(); |
226 | } else if (newDelegate.shouldRepaint(oldDelegate)) { |
227 | markNeedsPaint(); |
228 | } |
229 | |
230 | if (attached) { |
231 | oldDelegate._repaint?.removeListener(markNeedsPaint); |
232 | newDelegate._repaint?.addListener(markNeedsPaint); |
233 | } |
234 | } |
235 | |
236 | /// {@macro flutter.material.Material.clipBehavior} |
237 | /// |
238 | /// Defaults to [Clip.hardEdge]. |
239 | Clip get clipBehavior => _clipBehavior; |
240 | Clip _clipBehavior = Clip.hardEdge; |
241 | set clipBehavior(Clip value) { |
242 | if (value != _clipBehavior) { |
243 | _clipBehavior = value; |
244 | markNeedsPaint(); |
245 | markNeedsSemanticsUpdate(); |
246 | } |
247 | } |
248 | |
249 | @override |
250 | void attach(PipelineOwner owner) { |
251 | super.attach(owner); |
252 | _delegate._repaint?.addListener(markNeedsPaint); |
253 | } |
254 | |
255 | @override |
256 | void detach() { |
257 | _delegate._repaint?.removeListener(markNeedsPaint); |
258 | super.detach(); |
259 | } |
260 | |
261 | Size _getSize(BoxConstraints constraints) { |
262 | assert(constraints.debugAssertIsValid()); |
263 | return constraints.constrain(_delegate.getSize(constraints)); |
264 | } |
265 | |
266 | @override |
267 | bool get isRepaintBoundary => true; |
268 | |
269 | // TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to |
270 | // figure out the intrinsic dimensions. We really should either not support intrinsics, |
271 | // or we should expose intrinsic delegate callbacks and throw if they're not implemented. |
272 | |
273 | @override |
274 | double computeMinIntrinsicWidth(double height) { |
275 | final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width; |
276 | if (width.isFinite) { |
277 | return width; |
278 | } |
279 | return 0.0; |
280 | } |
281 | |
282 | @override |
283 | double computeMaxIntrinsicWidth(double height) { |
284 | final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width; |
285 | if (width.isFinite) { |
286 | return width; |
287 | } |
288 | return 0.0; |
289 | } |
290 | |
291 | @override |
292 | double computeMinIntrinsicHeight(double width) { |
293 | final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height; |
294 | if (height.isFinite) { |
295 | return height; |
296 | } |
297 | return 0.0; |
298 | } |
299 | |
300 | @override |
301 | double computeMaxIntrinsicHeight(double width) { |
302 | final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height; |
303 | if (height.isFinite) { |
304 | return height; |
305 | } |
306 | return 0.0; |
307 | } |
308 | |
309 | @override |
310 | @protected |
311 | Size computeDryLayout(covariant BoxConstraints constraints) { |
312 | return _getSize(constraints); |
313 | } |
314 | |
315 | @override |
316 | void performLayout() { |
317 | final BoxConstraints constraints = this.constraints; |
318 | size = _getSize(constraints); |
319 | int i = 0; |
320 | _randomAccessChildren.clear(); |
321 | RenderBox? child = firstChild; |
322 | while (child != null) { |
323 | _randomAccessChildren.add(child); |
324 | final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints); |
325 | child.layout(innerConstraints, parentUsesSize: true); |
326 | final FlowParentData childParentData = child.parentData! as FlowParentData; |
327 | childParentData.offset = Offset.zero; |
328 | child = childParentData.nextSibling; |
329 | i += 1; |
330 | } |
331 | } |
332 | |
333 | // Updated during layout. Only valid if layout is not dirty. |
334 | final List<RenderBox> _randomAccessChildren = <RenderBox>[]; |
335 | |
336 | // Updated during paint. |
337 | final List<int> _lastPaintOrder = <int>[]; |
338 | |
339 | // Only valid during paint. |
340 | PaintingContext? _paintingContext; |
341 | Offset? _paintingOffset; |
342 | |
343 | @override |
344 | Size? getChildSize(int i) { |
345 | if (i < 0 || i >= _randomAccessChildren.length) { |
346 | return null; |
347 | } |
348 | return _randomAccessChildren[i].size; |
349 | } |
350 | |
351 | @override |
352 | void paintChild(int i, {Matrix4? transform, double opacity = 1.0}) { |
353 | transform ??= Matrix4.identity(); |
354 | final RenderBox child = _randomAccessChildren[i]; |
355 | final FlowParentData childParentData = child.parentData! as FlowParentData; |
356 | assert(() { |
357 | if (childParentData._transform != null) { |
358 | throw FlutterError( |
359 | 'Cannot call paintChild twice for the same child.\n' |
360 | 'The flow delegate of type${_delegate.runtimeType} attempted to ' |
361 | 'paint child$i multiple times, which is not permitted.', |
362 | ); |
363 | } |
364 | return true; |
365 | }()); |
366 | _lastPaintOrder.add(i); |
367 | childParentData._transform = transform; |
368 | |
369 | // We return after assigning _transform so that the transparent child can |
370 | // still be hit tested at the correct location. |
371 | if (opacity == 0.0) { |
372 | return; |
373 | } |
374 | |
375 | void painter(PaintingContext context, Offset offset) { |
376 | context.paintChild(child, offset); |
377 | } |
378 | |
379 | if (opacity == 1.0) { |
380 | _paintingContext!.pushTransform(needsCompositing, _paintingOffset!, transform, painter); |
381 | } else { |
382 | _paintingContext!.pushOpacity(_paintingOffset!, ui.Color.getAlphaFromOpacity(opacity), ( |
383 | PaintingContext context, |
384 | Offset offset, |
385 | ) { |
386 | context.pushTransform(needsCompositing, offset, transform!, painter); |
387 | }); |
388 | } |
389 | } |
390 | |
391 | void _paintWithDelegate(PaintingContext context, Offset offset) { |
392 | _lastPaintOrder.clear(); |
393 | _paintingContext = context; |
394 | _paintingOffset = offset; |
395 | for (final RenderBox child in _randomAccessChildren) { |
396 | final FlowParentData childParentData = child.parentData! as FlowParentData; |
397 | childParentData._transform = null; |
398 | } |
399 | try { |
400 | _delegate.paintChildren(this); |
401 | } finally { |
402 | _paintingContext = null; |
403 | _paintingOffset = null; |
404 | } |
405 | } |
406 | |
407 | @override |
408 | void paint(PaintingContext context, Offset offset) { |
409 | _clipRectLayer.layer = context.pushClipRect( |
410 | needsCompositing, |
411 | offset, |
412 | Offset.zero & size, |
413 | _paintWithDelegate, |
414 | clipBehavior: clipBehavior, |
415 | oldLayer: _clipRectLayer.layer, |
416 | ); |
417 | } |
418 | |
419 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
420 | |
421 | @override |
422 | void dispose() { |
423 | _clipRectLayer.layer = null; |
424 | super.dispose(); |
425 | } |
426 | |
427 | @override |
428 | bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { |
429 | final List<RenderBox> children = getChildrenAsList(); |
430 | for (int i = _lastPaintOrder.length - 1; i >= 0; --i) { |
431 | final int childIndex = _lastPaintOrder[i]; |
432 | if (childIndex >= children.length) { |
433 | continue; |
434 | } |
435 | final RenderBox child = children[childIndex]; |
436 | final FlowParentData childParentData = child.parentData! as FlowParentData; |
437 | final Matrix4? transform = childParentData._transform; |
438 | if (transform == null) { |
439 | continue; |
440 | } |
441 | final bool absorbed = result.addWithPaintTransform( |
442 | transform: transform, |
443 | position: position, |
444 | hitTest: (BoxHitTestResult result, Offset position) { |
445 | return child.hitTest(result, position: position); |
446 | }, |
447 | ); |
448 | if (absorbed) { |
449 | return true; |
450 | } |
451 | } |
452 | return false; |
453 | } |
454 | |
455 | @override |
456 | void applyPaintTransform(RenderBox child, Matrix4 transform) { |
457 | final FlowParentData childParentData = child.parentData! as FlowParentData; |
458 | if (childParentData._transform != null) { |
459 | transform.multiply(childParentData._transform!); |
460 | } |
461 | super.applyPaintTransform(child, transform); |
462 | } |
463 | } |
464 |
Definitions
- FlowPaintingContext
- size
- childCount
- getChildSize
- paintChild
- FlowDelegate
- FlowDelegate
- getSize
- getConstraintsForChild
- paintChildren
- shouldRelayout
- shouldRepaint
- toString
- FlowParentData
- RenderFlow
- RenderFlow
- setupParentData
- delegate
- delegate
- clipBehavior
- clipBehavior
- attach
- detach
- _getSize
- isRepaintBoundary
- computeMinIntrinsicWidth
- computeMaxIntrinsicWidth
- computeMinIntrinsicHeight
- computeMaxIntrinsicHeight
- computeDryLayout
- performLayout
- getChildSize
- paintChild
- painter
- _paintWithDelegate
- paint
- dispose
- hitTestChildren
Learn more about Flutter for embedded and desktop on industrialflutter.com