From bf216110ffd37b02084b03eed2d2e713b58b2ab5 Mon Sep 17 00:00:00 2001 From: Simon Lightfoot Date: Mon, 16 Sep 2019 17:22:59 +0100 Subject: [PATCH] Adds relayout option to CustomMultiChildLayout. (#39252) --- AUTHORS | 3 +- .../lib/src/rendering/custom_layout.dart | 50 ++++++++++++++++--- .../lib/src/rendering/shifted_box.dart | 7 +-- .../custom_multi_child_layout_test.dart | 38 ++++++++++++++ 4 files changed, 87 insertions(+), 11 deletions(-) diff --git a/AUTHORS b/AUTHORS index 2a36d97be9..01ec32a3c9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -40,4 +40,5 @@ Frederik Schweiger Martin Staadecker Igor Katsuba Diego Velásquez -Sarbagya Dhaubanjar \ No newline at end of file +Simon Lightfoot +Sarbagya Dhaubanjar diff --git a/packages/flutter/lib/src/rendering/custom_layout.dart b/packages/flutter/lib/src/rendering/custom_layout.dart index 97686560f0..cd6c69e7fe 100644 --- a/packages/flutter/lib/src/rendering/custom_layout.dart +++ b/packages/flutter/lib/src/rendering/custom_layout.dart @@ -20,6 +20,9 @@ class MultiChildLayoutParentData extends ContainerBoxParentData { /// 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 @@ -38,8 +41,11 @@ class MultiChildLayoutParentData extends ContainerBoxParentData { /// Override [shouldRelayout] to determine when the layout of the children needs /// to be recomputed when the delegate changes. /// -/// Used with [CustomMultiChildLayout], the widget for the -/// [RenderCustomMultiChildLayoutBox] render object. +/// 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 @@ -94,7 +100,20 @@ class MultiChildLayoutParentData extends ContainerBoxParentData { /// 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 _idToChild; Set _debugChildrenNeedingLayout; @@ -300,13 +319,30 @@ class RenderCustomMultiChildLayoutBox extends RenderBox /// The delegate that controls the layout of the children. MultiChildLayoutDelegate get delegate => _delegate; MultiChildLayoutDelegate _delegate; - set delegate(MultiChildLayoutDelegate value) { - assert(value != null); - if (_delegate == value) + set delegate(MultiChildLayoutDelegate newDelegate) { + assert(newDelegate != null); + if (_delegate == newDelegate) return; - if (value.runtimeType != _delegate.runtimeType || value.shouldRelayout(_delegate)) + final MultiChildLayoutDelegate oldDelegate = _delegate; + if (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRelayout(oldDelegate)) markNeedsLayout(); - _delegate = value; + _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) { diff --git a/packages/flutter/lib/src/rendering/shifted_box.dart b/packages/flutter/lib/src/rendering/shifted_box.dart index 77b428661a..24400da7fe 100644 --- a/packages/flutter/lib/src/rendering/shifted_box.dart +++ b/packages/flutter/lib/src/rendering/shifted_box.dart @@ -962,10 +962,11 @@ class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox { /// is provided, to check if the new instance actually represents different /// information. /// -/// The most efficient way to trigger a relayout is to supply a relayout +/// The most efficient way to trigger a relayout is to supply a `relayout` /// argument to the constructor of the [SingleChildLayoutDelegate]. The custom -/// object will listen to this value and relayout whenever the animation -/// ticks, avoiding both the build phase of the pipeline. +/// 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. /// /// See also: /// diff --git a/packages/flutter/test/widgets/custom_multi_child_layout_test.dart b/packages/flutter/test/widgets/custom_multi_child_layout_test.dart index 1aa039880b..b0fe19fb27 100644 --- a/packages/flutter/test/widgets/custom_multi_child_layout_test.dart +++ b/packages/flutter/test/widgets/custom_multi_child_layout_test.dart @@ -73,6 +73,23 @@ class PreferredSizeDelegate extends MultiChildLayoutDelegate { } } +class NotifierLayoutDelegate extends MultiChildLayoutDelegate { + NotifierLayoutDelegate(this.size) : super(relayout: size); + + final ValueNotifier size; + + @override + Size getSize(BoxConstraints constraints) => size.value; + + @override + void performLayout(Size size) { } + + @override + bool shouldRelayout(NotifierLayoutDelegate oldDelegate) { + return size != oldDelegate.size; + } +} + void main() { testWidgets('Control test for CustomMultiChildLayout', (WidgetTester tester) async { final TestMultiChildLayoutDelegate delegate = TestMultiChildLayoutDelegate(); @@ -161,4 +178,25 @@ void main() { expect(box.size.width, equals(350.0)); expect(box.size.height, equals(250.0)); }); + + testWidgets('Can use listener for relayout', (WidgetTester tester) async { + final ValueNotifier size = ValueNotifier(const Size(100.0, 200.0)); + + await tester.pumpWidget( + Center( + child: CustomMultiChildLayout( + delegate: NotifierLayoutDelegate(size), + ), + ), + ); + + RenderBox box = tester.renderObject(find.byType(CustomMultiChildLayout)); + expect(box.size, equals(const Size(100.0, 200.0))); + + size.value = const Size(150.0, 240.0); + await tester.pump(); + + box = tester.renderObject(find.byType(CustomMultiChildLayout)); + expect(box.size, equals(const Size(150.0, 240.0))); + }); }