diff --git a/examples/api/lib/widgets/basic/custom_multi_child_layout.0.dart b/examples/api/lib/widgets/basic/custom_multi_child_layout.0.dart new file mode 100644 index 00000000000..332754e19c0 --- /dev/null +++ b/examples/api/lib/widgets/basic/custom_multi_child_layout.0.dart @@ -0,0 +1,124 @@ +// 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. + +// Flutter code sample for CustomMultiChildLayout + +import 'package:flutter/material.dart'; + +void main() => runApp(const ExampleApp()); + +class ExampleApp extends StatelessWidget { + const ExampleApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Directionality( + // TRY THIS: Try changing the direction here and hot-reloading to + // see the layout change. + textDirection: TextDirection.ltr, + child: Scaffold( + body: ExampleWidget(), + ), + ), + ); + } +} + +/// Lays out the children in a cascade, where the top corner of the next child +/// is a little above (`overlap`) the lower end corner of the previous child. +/// +/// Will relayout if the text direction changes. +class _CascadeLayoutDelegate extends MultiChildLayoutDelegate { + _CascadeLayoutDelegate({ + required this.colors, + required this.overlap, + required this.textDirection, + }); + + final Map colors; + final double overlap; + final TextDirection textDirection; + + // Perform layout will be called when re-layout is needed. + @override + void performLayout(Size size) { + final double columnWidth = size.width / colors.length; + Offset childPosition = Offset.zero; + switch (textDirection) { + case TextDirection.rtl: + childPosition += Offset(size.width, 0); + break; + case TextDirection.ltr: + break; + } + for (final String color in colors.keys) { + // layoutChild must be called exactly once for each child. + final Size currentSize = layoutChild( + color, + BoxConstraints(maxHeight: size.height, maxWidth: columnWidth), + ); + // positionChild must be called to change the position of a child from + // what it was in the previous layout. Each child starts at (0, 0) for the + // first layout. + switch (textDirection) { + case TextDirection.rtl: + positionChild(color, childPosition - Offset(currentSize.width, 0)); + childPosition += Offset(-currentSize.width, currentSize.height - overlap); + break; + case TextDirection.ltr: + positionChild(color, childPosition); + childPosition += Offset(currentSize.width, currentSize.height - overlap); + break; + } + } + } + + // shouldRelayout is called to see if the delegate has changed and requires a + // layout to occur. Should only return true if the delegate state itself + // changes: changes in the CustomMultiChildLayout attributes will + // automatically cause a relayout, like any other widget. + @override + bool shouldRelayout(_CascadeLayoutDelegate oldDelegate) { + return oldDelegate.textDirection != textDirection + || oldDelegate.overlap != overlap; + } +} + +class ExampleWidget extends StatelessWidget { + const ExampleWidget({Key? key}) : super(key: key); + + static const Map _colors = { + 'Red': Colors.red, + 'Green': Colors.green, + 'Blue': Colors.blue, + 'Cyan': Colors.cyan, + }; + + @override + Widget build(BuildContext context) { + return CustomMultiChildLayout( + delegate: _CascadeLayoutDelegate( + colors: _colors, + overlap: 30.0, + textDirection: Directionality.of(context), + ), + children: [ + // Create all of the colored boxes in the colors map. + for (MapEntry entry in _colors.entries) + // The "id" can be any Object, not just a String. + LayoutId( + id: entry.key, + child: Container( + color: entry.value, + width: 100.0, + height: 100.0, + alignment: Alignment.center, + child: Text(entry.key), + ), + ), + ], + ); + } +} diff --git a/examples/api/test/widgets/basic/custom_multi_child_layout.0_test.dart b/examples/api/test/widgets/basic/custom_multi_child_layout.0_test.dart new file mode 100644 index 00000000000..c762a58f402 --- /dev/null +++ b/examples/api/test/widgets/basic/custom_multi_child_layout.0_test.dart @@ -0,0 +1,59 @@ +// 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/material.dart'; +import 'package:flutter_api_samples/widgets/basic/custom_multi_child_layout.0.dart' + as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('has four containers', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: example.ExampleWidget(), + ), + ), + ); + final Finder containerFinder = find.byType(Container); + expect(containerFinder, findsNWidgets(4)); + }); + + testWidgets('containers are the same size', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: example.ExampleWidget(), + ), + ), + ); + final Finder containerFinder = find.byType(Container); + const Size expectedSize = Size(100, 100); + for (int i = 0; i < 4; i += 1) { + expect(tester.getSize(containerFinder.at(i)), equals(expectedSize)); + } + expect(containerFinder, findsNWidgets(4)); + }); + + testWidgets('containers are offset', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: example.ExampleWidget(), + ), + ), + ); + final Finder containerFinder = find.byType(Container); + Rect previousRect = tester.getRect(containerFinder.first); + for (int i = 1; i < 4; i += 1) { + expect( + tester.getRect(containerFinder.at(i)), + equals(previousRect.shift(const Offset(100, 70))), + reason: 'Rect $i not correct size', + ); + previousRect = tester.getRect(containerFinder.at(i)); + } + expect(containerFinder, findsNWidgets(4)); + }); +} diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 775faebf764..cf2502663ee 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -2138,6 +2138,15 @@ class LayoutId extends ParentDataWidget { /// Each child must be wrapped in a [LayoutId] widget to identify the widget for /// the delegate. /// +/// {@tool dartpad} +/// This example shows a [CustomMultiChildLayout] widget being used to lay out +/// colored blocks from start to finish in a cascade that has some overlap. +/// +/// It responds to changes in [Directionality] by re-laying out its children. +/// +/// ** See code in examples/api/lib/widgets/basic/custom_multi_child_layout.0.dart ** +/// {@end-tool} +/// /// See also: /// /// * [MultiChildLayoutDelegate], for details about how to control the layout of