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
5import 'dart:ui' as ui show Color;
6
7import 'package:flutter/foundation.dart';
8import 'package:vector_math/vector_math_64.dart';
9
10import 'box.dart';
11import 'layer.dart';
12import '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]
24abstract 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]
59abstract 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.
146class 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]
174class 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