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