404 lines
15 KiB
Dart
404 lines
15 KiB
Dart
// 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 'package:flutter/foundation.dart';
|
|
|
|
import 'box.dart';
|
|
import 'object.dart';
|
|
|
|
// For SingleChildLayoutDelegate and RenderCustomSingleChildLayoutBox, see shifted_box.dart
|
|
|
|
/// [ParentData] used by [RenderCustomMultiChildLayoutBox].
|
|
class MultiChildLayoutParentData extends ContainerBoxParentData<RenderBox> {
|
|
/// An object representing the identity of this child.
|
|
Object id;
|
|
|
|
@override
|
|
String toString() => '${super.toString()}; id=$id';
|
|
}
|
|
|
|
/// A delegate that controls the layout of multiple children.
|
|
///
|
|
/// Used with [CustomMultiChildLayout] (in the widgets library) and
|
|
/// [RenderCustomMultiChildLayoutBox] (in the rendering library).
|
|
///
|
|
/// Delegates must be idempotent. Specifically, if two delegates are equal, then
|
|
/// they must produce the same layout. To change the layout, replace the
|
|
/// delegate with a different instance whose [shouldRelayout] returns true when
|
|
/// given the previous instance.
|
|
///
|
|
/// Override [getSize] to control the overall size of the layout. The size of
|
|
/// the layout cannot depend on layout properties of the children.
|
|
///
|
|
/// Override [performLayout] to size and position the children. An
|
|
/// implementation of [performLayout] must call [layoutChild] exactly once for
|
|
/// each child, but it may call [layoutChild] on children in an arbitrary order.
|
|
/// Typically a delegate will use the size returned from [layoutChild] on one
|
|
/// child to determine the constraints for [performLayout] on another child or
|
|
/// to determine the offset for [positionChild] for that child or another child.
|
|
///
|
|
/// Override [shouldRelayout] to determine when the layout of the children needs
|
|
/// to be recomputed when the delegate changes.
|
|
///
|
|
/// The most efficient way to trigger a relayout is to supply a `relayout`
|
|
/// argument to the constructor of the [MultiChildLayoutDelegate]. The custom
|
|
/// layout will listen to this value and relayout whenever the Listenable
|
|
/// notifies its listeners, such as when an [Animation] ticks. This allows
|
|
/// the custom layout to avoid the build phase of the pipeline.
|
|
///
|
|
/// Each child must be wrapped in a [LayoutId] widget to assign the id that
|
|
/// identifies it to the delegate. The [LayoutId.id] needs to be unique among
|
|
/// the children that the [CustomMultiChildLayout] manages.
|
|
///
|
|
/// {@tool sample}
|
|
///
|
|
/// Below is an example implementation of [performLayout] that causes one widget
|
|
/// (the follower) to be the same size as another (the leader):
|
|
///
|
|
/// ```dart
|
|
/// // Define your own slot numbers, depending upon the id assigned by LayoutId.
|
|
/// // Typical usage is to define an enum like the one below, and use those
|
|
/// // values as the ids.
|
|
/// enum _Slot {
|
|
/// leader,
|
|
/// follower,
|
|
/// }
|
|
///
|
|
/// class FollowTheLeader extends MultiChildLayoutDelegate {
|
|
/// @override
|
|
/// void performLayout(Size size) {
|
|
/// Size leaderSize = Size.zero;
|
|
///
|
|
/// if (hasChild(_Slot.leader)) {
|
|
/// leaderSize = layoutChild(_Slot.leader, BoxConstraints.loose(size));
|
|
/// positionChild(_Slot.leader, Offset.zero);
|
|
/// }
|
|
///
|
|
/// if (hasChild(_Slot.follower)) {
|
|
/// layoutChild(_Slot.follower, BoxConstraints.tight(leaderSize));
|
|
/// positionChild(_Slot.follower, Offset(size.width - leaderSize.width,
|
|
/// size.height - leaderSize.height));
|
|
/// }
|
|
/// }
|
|
///
|
|
/// @override
|
|
/// bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
|
|
/// }
|
|
/// ```
|
|
/// {@end-tool}
|
|
///
|
|
/// The delegate gives the leader widget loose constraints, which means the
|
|
/// child determines what size to be (subject to fitting within the given size).
|
|
/// The delegate then remembers the size of that child and places it in the
|
|
/// upper left corner.
|
|
///
|
|
/// The delegate then gives the follower widget tight constraints, forcing it to
|
|
/// match the size of the leader widget. The delegate then places the follower
|
|
/// widget in the bottom right corner.
|
|
///
|
|
/// The leader and follower widget will paint in the order they appear in the
|
|
/// child list, regardless of the order in which [layoutChild] is called on
|
|
/// them.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [CustomMultiChildLayout], the widget that uses this delegate.
|
|
/// * [RenderCustomMultiChildLayoutBox], render object that uses this
|
|
/// delegate.
|
|
abstract class MultiChildLayoutDelegate {
|
|
/// Creates a layout delegate.
|
|
///
|
|
/// The layout will update whenever [relayout] notifies its listeners.
|
|
MultiChildLayoutDelegate({ Listenable relayout }) : _relayout = relayout;
|
|
|
|
final Listenable _relayout;
|
|
|
|
Map<Object, RenderBox> _idToChild;
|
|
Set<RenderBox> _debugChildrenNeedingLayout;
|
|
|
|
/// True if a non-null LayoutChild was provided for the specified id.
|
|
///
|
|
/// Call this from the [performLayout] or [getSize] methods to
|
|
/// determine which children are available, if the child list might
|
|
/// vary.
|
|
bool hasChild(Object childId) => _idToChild[childId] != null;
|
|
|
|
/// Ask the child to update its layout within the limits specified by
|
|
/// the constraints parameter. The child's size is returned.
|
|
///
|
|
/// Call this from your [performLayout] function to lay out each
|
|
/// child. Every child must be laid out using this function exactly
|
|
/// once each time the [performLayout] function is called.
|
|
Size layoutChild(Object childId, BoxConstraints constraints) {
|
|
final RenderBox child = _idToChild[childId];
|
|
assert(() {
|
|
if (child == null) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('The $this custom multichild layout delegate tried to lay out a non-existent child.'),
|
|
ErrorDescription('There is no child with the id "$childId".')
|
|
]);
|
|
}
|
|
if (!_debugChildrenNeedingLayout.remove(child)) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('The $this custom multichild layout delegate tried to lay out the child with id "$childId" more than once.'),
|
|
ErrorDescription('Each child must be laid out exactly once.')
|
|
]);
|
|
}
|
|
try {
|
|
assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
|
|
} on AssertionError catch (exception) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".'),
|
|
DiagnosticsProperty<AssertionError>('Exception', exception, showName: false),
|
|
ErrorDescription(
|
|
'The minimum width and height must be greater than or equal to zero.\n'
|
|
'The maximum width must be greater than or equal to the minimum width.\n'
|
|
'The maximum height must be greater than or equal to the minimum height.'
|
|
)
|
|
]);
|
|
}
|
|
return true;
|
|
}());
|
|
child.layout(constraints, parentUsesSize: true);
|
|
return child.size;
|
|
}
|
|
|
|
/// Specify the child's origin relative to this origin.
|
|
///
|
|
/// Call this from your [performLayout] function to position each
|
|
/// child. If you do not call this for a child, its position will
|
|
/// remain unchanged. Children initially have their position set to
|
|
/// (0,0), i.e. the top left of the [RenderCustomMultiChildLayoutBox].
|
|
void positionChild(Object childId, Offset offset) {
|
|
final RenderBox child = _idToChild[childId];
|
|
assert(() {
|
|
if (child == null) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('The $this custom multichild layout delegate tried to position out a non-existent child:'),
|
|
ErrorDescription('There is no child with the id "$childId".')
|
|
]);
|
|
}
|
|
if (offset == null) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('The $this custom multichild layout delegate provided a null position for the child with id "$childId".')
|
|
]);
|
|
}
|
|
return true;
|
|
}());
|
|
final MultiChildLayoutParentData childParentData = child.parentData as MultiChildLayoutParentData;
|
|
childParentData.offset = offset;
|
|
}
|
|
|
|
DiagnosticsNode _debugDescribeChild(RenderBox child) {
|
|
final MultiChildLayoutParentData childParentData = child.parentData as MultiChildLayoutParentData;
|
|
return DiagnosticsProperty<RenderBox>('${childParentData.id}', child);
|
|
}
|
|
|
|
void _callPerformLayout(Size size, RenderBox firstChild) {
|
|
// A particular layout delegate could be called reentrantly, e.g. if it used
|
|
// by both a parent and a child. So, we must restore the _idToChild map when
|
|
// we return.
|
|
final Map<Object, RenderBox> previousIdToChild = _idToChild;
|
|
|
|
Set<RenderBox> debugPreviousChildrenNeedingLayout;
|
|
assert(() {
|
|
debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
|
|
_debugChildrenNeedingLayout = <RenderBox>{};
|
|
return true;
|
|
}());
|
|
|
|
try {
|
|
_idToChild = <Object, RenderBox>{};
|
|
RenderBox child = firstChild;
|
|
while (child != null) {
|
|
final MultiChildLayoutParentData childParentData = child.parentData as MultiChildLayoutParentData;
|
|
assert(() {
|
|
if (childParentData.id == null) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.'),
|
|
child.describeForError('The following child has no ID'),
|
|
]);
|
|
}
|
|
return true;
|
|
}());
|
|
_idToChild[childParentData.id] = child;
|
|
assert(() {
|
|
_debugChildrenNeedingLayout.add(child);
|
|
return true;
|
|
}());
|
|
child = childParentData.nextSibling;
|
|
}
|
|
performLayout(size);
|
|
assert(() {
|
|
if (_debugChildrenNeedingLayout.isNotEmpty) {
|
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
|
ErrorSummary('Each child must be laid out exactly once.'),
|
|
DiagnosticsBlock(
|
|
name:
|
|
'The $this custom multichild layout delegate forgot '
|
|
'to lay out the following '
|
|
'${_debugChildrenNeedingLayout.length > 1 ? 'children' : 'child'}',
|
|
properties: _debugChildrenNeedingLayout.map<DiagnosticsNode>(_debugDescribeChild).toList(),
|
|
style: DiagnosticsTreeStyle.whitespace,
|
|
),
|
|
]);
|
|
}
|
|
return true;
|
|
}());
|
|
} finally {
|
|
_idToChild = previousIdToChild;
|
|
assert(() {
|
|
_debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
|
|
return true;
|
|
}());
|
|
}
|
|
}
|
|
|
|
/// Override this method to return the size of this object given the
|
|
/// incoming constraints.
|
|
///
|
|
/// The size cannot reflect the sizes of the children. If this layout has a
|
|
/// fixed width or height the returned size can reflect that; the size will be
|
|
/// constrained to the given constraints.
|
|
///
|
|
/// By default, attempts to size the box to the biggest size
|
|
/// possible given the constraints.
|
|
Size getSize(BoxConstraints constraints) => constraints.biggest;
|
|
|
|
/// Override this method to lay out and position all children given this
|
|
/// widget's size.
|
|
///
|
|
/// This method must call [layoutChild] for each child. It should also specify
|
|
/// the final position of each child with [positionChild].
|
|
void performLayout(Size size);
|
|
|
|
/// 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 MultiChildLayoutDelegate 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() => '$runtimeType';
|
|
}
|
|
|
|
/// Defers the layout of multiple children to a delegate.
|
|
///
|
|
/// The delegate can determine the layout constraints for each child and can
|
|
/// decide where to position each child. The delegate can also determine the
|
|
/// size of the parent, but the size of the parent cannot depend on the sizes of
|
|
/// the children.
|
|
class RenderCustomMultiChildLayoutBox extends RenderBox
|
|
with ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
|
|
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
|
|
/// Creates a render object that customizes the layout of multiple children.
|
|
///
|
|
/// The [delegate] argument must not be null.
|
|
RenderCustomMultiChildLayoutBox({
|
|
List<RenderBox> children,
|
|
@required MultiChildLayoutDelegate delegate,
|
|
}) : assert(delegate != null),
|
|
_delegate = delegate {
|
|
addAll(children);
|
|
}
|
|
|
|
@override
|
|
void setupParentData(RenderBox child) {
|
|
if (child.parentData is! MultiChildLayoutParentData)
|
|
child.parentData = MultiChildLayoutParentData();
|
|
}
|
|
|
|
/// The delegate that controls the layout of the children.
|
|
MultiChildLayoutDelegate get delegate => _delegate;
|
|
MultiChildLayoutDelegate _delegate;
|
|
set delegate(MultiChildLayoutDelegate newDelegate) {
|
|
assert(newDelegate != null);
|
|
if (_delegate == newDelegate)
|
|
return;
|
|
final MultiChildLayoutDelegate oldDelegate = _delegate;
|
|
if (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRelayout(oldDelegate))
|
|
markNeedsLayout();
|
|
_delegate = newDelegate;
|
|
if (attached) {
|
|
oldDelegate?._relayout?.removeListener(markNeedsLayout);
|
|
newDelegate?._relayout?.addListener(markNeedsLayout);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void attach(PipelineOwner owner) {
|
|
super.attach(owner);
|
|
_delegate?._relayout?.addListener(markNeedsLayout);
|
|
}
|
|
|
|
@override
|
|
void detach() {
|
|
_delegate?._relayout?.removeListener(markNeedsLayout);
|
|
super.detach();
|
|
}
|
|
|
|
Size _getSize(BoxConstraints constraints) {
|
|
assert(constraints.debugAssertIsValid());
|
|
return constraints.constrain(_delegate.getSize(constraints));
|
|
}
|
|
|
|
// 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() {
|
|
size = _getSize(constraints);
|
|
delegate._callPerformLayout(size, firstChild);
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
defaultPaint(context, offset);
|
|
}
|
|
|
|
@override
|
|
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
|
|
return defaultHitTestChildren(result, position: position);
|
|
}
|
|
}
|