// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' as ui show Color; import 'package:flutter/foundation.dart'; import 'package:vector_math/vector_math_64.dart'; import 'box.dart'; import 'layer.dart'; import 'object.dart'; /// A context in which a [FlowDelegate] paints. /// /// Provides information about the current size of the container and the /// children and a mechanism for painting children. /// /// See also: /// /// * [FlowDelegate] /// * [Flow] /// * [RenderFlow] abstract class FlowPaintingContext { /// The size of the container in which the children can be painted. Size get size; /// The number of children available to paint. int get childCount; /// The size of the [i]th child. /// /// If [i] is negative or exceeds [childCount], returns null. Size? getChildSize(int i); /// Paint the [i]th child using the given transform. /// /// The child will be painted in a coordinate system that concatenates the /// container's coordinate system with the given transform. The origin of the /// parent's coordinate system is the upper left corner of the parent, with /// x increasing rightward and y increasing downward. /// /// The container will clip the children to its bounds. void paintChild(int i, { Matrix4 transform, double opacity = 1.0 }); } /// A delegate that controls the appearance of a flow layout. /// /// Flow layouts are optimized for moving children around the screen using /// transformation matrices. For optimal performance, construct the /// [FlowDelegate] with an [Animation] that ticks whenever the delegate wishes /// to change the transformation matrices for the children and avoid rebuilding /// the [Flow] widget itself every animation frame. /// /// See also: /// /// * [Flow] /// * [RenderFlow] abstract class FlowDelegate { /// The flow will repaint whenever [repaint] notifies its listeners. const FlowDelegate({ Listenable? repaint }) : _repaint = repaint; final Listenable? _repaint; /// Override to control the size of the container for the children. /// /// By default, the flow will be as large as possible. If this function /// returns a size that does not respect the given constraints, the size will /// be adjusted to be as close to the returned size as possible while still /// respecting the constraints. /// /// If this function depends on information other than the given constraints, /// override [shouldRelayout] to indicate when when the container should /// relayout. Size getSize(BoxConstraints constraints) => constraints.biggest; /// Override to control the layout constraints given to each child. /// /// By default, the children will receive the given constraints, which are the /// constraints used to size the container. The children need /// not respect the given constraints, but they are required to respect the /// returned constraints. For example, the incoming constraints might require /// the container to have a width of exactly 100.0 and a height of exactly /// 100.0, but this function might give the children looser constraints that /// let them be larger or smaller than 100.0 by 100.0. /// /// If this function depends on information other than the given constraints, /// override [shouldRelayout] to indicate when when the container should /// relayout. BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) => constraints; /// Override to paint the children of the flow. /// /// Children can be painted in any order, but each child can be painted at /// most once. Although the container clips the children to its own bounds, it /// is more efficient to skip painting a child altogether rather than having /// it paint entirely outside the container's clip. /// /// To paint a child, call [FlowPaintingContext.paintChild] on the given /// [FlowPaintingContext] (the `context` argument). The given context is valid /// only within the scope of this function call and contains information (such /// as the size of the container) that is useful for picking transformation /// matrices for the children. /// /// If this function depends on information other than the given context, /// override [shouldRepaint] to indicate when when the container should /// relayout. void paintChildren(FlowPaintingContext context); /// Override this method to return true when the children need to be laid out. /// This should compare the fields of the current delegate and the given /// oldDelegate and return true if the fields are such that the layout would /// be different. bool shouldRelayout(covariant FlowDelegate oldDelegate) => false; /// Override this method to return true when the children need to be /// repainted. This should compare the fields of the current delegate and the /// given oldDelegate and return true if the fields are such that /// paintChildren would act differently. /// /// The delegate can also trigger a repaint if the delegate provides the /// repaint animation argument to this object's constructor and that animation /// ticks. Triggering a repaint using this animation-based mechanism is more /// efficient than rebuilding the [Flow] widget to change its delegate. /// /// The flow container might repaint even if this function returns false, for /// example if layout triggers painting (e.g., if [shouldRelayout] returns /// true). bool shouldRepaint(covariant FlowDelegate oldDelegate); /// Override this method to include additional information in the /// debugging data printed by [debugDumpRenderTree] and friends. /// /// By default, returns the [runtimeType] of the class. @override String toString() => objectRuntimeType(this, 'FlowDelegate'); } /// Parent data for use with [RenderFlow]. /// /// The [offset] property is ignored by [RenderFlow] and is always set to /// [Offset.zero]. Children of a [RenderFlow] are positioned using a /// transformation matrix, which is private to the [RenderFlow]. To set the /// matrix, use the [FlowPaintingContext.paintChild] function from an override /// of the [FlowDelegate.paintChildren] function. class FlowParentData extends ContainerBoxParentData<RenderBox> { Matrix4? _transform; } /// Implements the flow layout algorithm. /// /// Flow layouts are optimized for repositioning children using transformation /// matrices. /// /// The flow container is sized independently from the children by the /// [FlowDelegate.getSize] function of the delegate. The children are then sized /// independently given the constraints from the /// [FlowDelegate.getConstraintsForChild] function. /// /// Rather than positioning the children during layout, the children are /// positioned using transformation matrices during the paint phase using the /// matrices from the [FlowDelegate.paintChildren] function. The children can be /// repositioned efficiently by simply repainting the flow. /// /// The most efficient way to trigger a repaint of the flow is to supply a /// repaint argument to the constructor of the [FlowDelegate]. The flow will /// listen to this animation and repaint whenever the animation ticks, avoiding /// both the build and layout phases of the pipeline. /// /// See also: /// /// * [FlowDelegate] /// * [RenderStack] class RenderFlow extends RenderBox with ContainerRenderObjectMixin<RenderBox, FlowParentData>, RenderBoxContainerDefaultsMixin<RenderBox, FlowParentData> implements FlowPaintingContext { /// Creates a render object for a flow layout. /// /// For optimal performance, consider using children that return true from /// [isRepaintBoundary]. RenderFlow({ List<RenderBox>? children, required FlowDelegate delegate, Clip clipBehavior = Clip.hardEdge, }) : assert(delegate != null), assert(clipBehavior != null), _delegate = delegate, _clipBehavior = clipBehavior { addAll(children); } @override void setupParentData(RenderBox child) { final ParentData? childParentData = child.parentData; if (childParentData is FlowParentData) childParentData._transform = null; else child.parentData = FlowParentData(); } /// The delegate that controls the transformation matrices of the children. FlowDelegate get delegate => _delegate; FlowDelegate _delegate; /// When the delegate is changed to a new delegate with the same runtimeType /// as the old delegate, this object will call the delegate's /// [FlowDelegate.shouldRelayout] and [FlowDelegate.shouldRepaint] functions /// to determine whether the new delegate requires this object to update its /// layout or painting. set delegate(FlowDelegate newDelegate) { assert(newDelegate != null); if (_delegate == newDelegate) return; final FlowDelegate oldDelegate = _delegate; _delegate = newDelegate; if (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRelayout(oldDelegate)) markNeedsLayout(); else if (newDelegate.shouldRepaint(oldDelegate)) markNeedsPaint(); if (attached) { oldDelegate._repaint?.removeListener(markNeedsPaint); newDelegate._repaint?.addListener(markNeedsPaint); } } /// {@macro flutter.material.Material.clipBehavior} /// /// Defaults to [Clip.hardEdge], and must not be null. Clip get clipBehavior => _clipBehavior; Clip _clipBehavior = Clip.hardEdge; set clipBehavior(Clip value) { assert(value != null); if (value != _clipBehavior) { _clipBehavior = value; markNeedsPaint(); markNeedsSemanticsUpdate(); } } @override void attach(PipelineOwner owner) { super.attach(owner); _delegate._repaint?.addListener(markNeedsPaint); } @override void detach() { _delegate._repaint?.removeListener(markNeedsPaint); super.detach(); } Size _getSize(BoxConstraints constraints) { assert(constraints.debugAssertIsValid()); return constraints.constrain(_delegate.getSize(constraints)); } @override bool get isRepaintBoundary => true; // TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to // figure out the intrinsic dimensions. We really should either not support intrinsics, // or we should expose intrinsic delegate callbacks and throw if they're not implemented. @override double computeMinIntrinsicWidth(double height) { final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width; if (width.isFinite) return width; return 0.0; } @override double computeMaxIntrinsicWidth(double height) { final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width; if (width.isFinite) return width; return 0.0; } @override double computeMinIntrinsicHeight(double width) { final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height; if (height.isFinite) return height; return 0.0; } @override double computeMaxIntrinsicHeight(double width) { final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height; if (height.isFinite) return height; return 0.0; } @override void performLayout() { final BoxConstraints constraints = this.constraints; size = _getSize(constraints); int i = 0; _randomAccessChildren.clear(); RenderBox? child = firstChild; while (child != null) { _randomAccessChildren.add(child); final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints); child.layout(innerConstraints, parentUsesSize: true); final FlowParentData childParentData = child.parentData! as FlowParentData; childParentData.offset = Offset.zero; child = childParentData.nextSibling; i += 1; } } // Updated during layout. Only valid if layout is not dirty. final List<RenderBox> _randomAccessChildren = <RenderBox>[]; // Updated during paint. final List<int> _lastPaintOrder = <int>[]; // Only valid during paint. PaintingContext? _paintingContext; Offset? _paintingOffset; @override Size? getChildSize(int i) { if (i < 0 || i >= _randomAccessChildren.length) return null; return _randomAccessChildren[i].size; } @override void paintChild(int i, { Matrix4? transform, double opacity = 1.0 }) { transform ??= Matrix4.identity(); final RenderBox child = _randomAccessChildren[i]; final FlowParentData childParentData = child.parentData! as FlowParentData; assert(() { if (childParentData._transform != null) { throw FlutterError( 'Cannot call paintChild twice for the same child.\n' 'The flow delegate of type ${_delegate.runtimeType} attempted to ' 'paint child $i multiple times, which is not permitted.' ); } return true; }()); _lastPaintOrder.add(i); childParentData._transform = transform; // We return after assigning _transform so that the transparent child can // still be hit tested at the correct location. if (opacity == 0.0) return; void painter(PaintingContext context, Offset offset) { context.paintChild(child, offset); } if (opacity == 1.0) { _paintingContext!.pushTransform(needsCompositing, _paintingOffset!, transform, painter); } else { _paintingContext!.pushOpacity(_paintingOffset!, ui.Color.getAlphaFromOpacity(opacity), (PaintingContext context, Offset offset) { context.pushTransform(needsCompositing, offset, transform!, painter); }); } } void _paintWithDelegate(PaintingContext context, Offset offset) { _lastPaintOrder.clear(); _paintingContext = context; _paintingOffset = offset; for (final RenderBox child in _randomAccessChildren) { final FlowParentData childParentData = child.parentData! as FlowParentData; childParentData._transform = null; } try { _delegate.paintChildren(this); } finally { _paintingContext = null; _paintingOffset = null; } } @override void paint(PaintingContext context, Offset offset) { if (clipBehavior == Clip.none) { _clipRectLayer = null; _paintWithDelegate(context, offset); } else { _clipRectLayer = context.pushClipRect(needsCompositing, offset, Offset.zero & size, _paintWithDelegate, clipBehavior: clipBehavior, oldLayer: _clipRectLayer); } } ClipRectLayer? _clipRectLayer; @override bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { final List<RenderBox> children = getChildrenAsList(); for (int i = _lastPaintOrder.length - 1; i >= 0; --i) { final int childIndex = _lastPaintOrder[i]; if (childIndex >= children.length) continue; final RenderBox child = children[childIndex]; final FlowParentData childParentData = child.parentData! as FlowParentData; final Matrix4? transform = childParentData._transform; if (transform == null) continue; final bool absorbed = result.addWithPaintTransform( transform: transform, position: position, hitTest: (BoxHitTestResult result, Offset? position) { return child.hitTest(result, position: position!); }, ); if (absorbed) return true; } return false; } @override void applyPaintTransform(RenderBox child, Matrix4 transform) { final FlowParentData childParentData = child.parentData! as FlowParentData; if (childParentData._transform != null) transform.multiply(childParentData._transform!); super.applyPaintTransform(child, transform); } }