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';
8library;
9
10import 'dart:ui' as ui show Color;
11
12import 'package:flutter/foundation.dart';
13import 'package:vector_math/vector_math_64.dart';
14
15import 'box.dart';
16import 'layer.dart';
17import '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]
29abstract 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]
64abstract 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.
151class 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]
179class 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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com