How to implement advanced layouts in Flutter

20 January 2021 | ~6 min read
Can we position widgets within custom shapes in Flutter? Actually, the framework provides a solution for building any kind of layout logic using CustomChildLayouts. I will show you how to implement a CustomMultiChildLayout to center any widget within the slice of a circle. How to draw that slice using a CustomPainter is shown in [my previous article](https://www.kevlatus.de/blog/making-of-flutter-fortune-wheel) in this series. This example is relevant, since all widgets in Flutter are based on rectangular boxes with each slice actually having the following bounding box. ![https://raw.githubusercontent.com/kevlatus/kevlatus.de/main/public/assets/blog/images/img-circle-slice-children.png](https://raw.githubusercontent.com/kevlatus/kevlatus.de/main/public/assets/blog/images/img-circle-slice-children.png) Therefore, we cannot use widgets like `Center` or `Align` in this scenario, since they position relative to the bounding box. Time to introduce the `MultiChildLayoutDelegate`: a base class for providing your own layout logic by implementing the `performLayout(Size)` method. Within this method two tasks need to be performed: 1. all children are given their box constraints 2. the resulting boxes are positioned within the available bounds To identify child widgets, the framework provides us with the `hasChild` method. In order for them to work properly, each child needs to be given an ID, which can be understood as the *role* it takes within the layout. The following code shows how this is achieved using the `LayoutId` widget. ```dart enum CircleSliceLayoutSlot { Slice, Child, } class CircleSliceLayout extends StatelessWidget { final Widget child; final CircleSlice slice; const CircleSliceLayout({this.slice, this.child}); @override Widget build(BuildContext context) { return CustomMultiChildLayout( delegate: CircleSliceLayoutDelegate(slice.angle), children: [ LayoutId( // ⬅ ⬇ id: CircleSliceLayoutSlot.Slice, child: slice, ), LayoutId( // ⬅ ⬇ id: CircleSliceLayoutSlot.Child, child: Transform.rotate( angle: slice.angle / 2, child: child, ), ), ], ); } } ``` An enum helps us to distinguish between the circle slice and its child widget, which makes the code more readable and maintainable, but you could also use raw values/constants. Now let's look at the `CircleSliceLayoutDelegate` implementation. ```dart class CircleSliceLayoutDelegate extends MultiChildLayoutDelegate { final double angle; CircleSliceLayoutDelegate(this.angle); @override void performLayout(Size size) { Size sliceSize; Size childSize; if (hasChild(CircleSliceLayoutSlot.Slice)) { sliceSize = layoutChild( CircleSliceLayoutSlot.Slice, BoxConstraints.tight(size), ); positionChildCircleSliceLayoutSlot.Slice, Offset.zero); } if (hasChild(CircleSliceLayoutSlot.Child)) { childSize = layoutChild( CircleSliceLayoutSlot.Child, BoxConstraints.loose(size), ); final topRectVector = Math.Point(sliceSize.width / 2, 0.0); final halfAngleVector = rotateVector(topRectVector, angle / 2); positionChild( CircleSliceLayoutSlot.Child, Offset( halfAngleVector.x - childSize.width / 2, halfAngleVector.y - childSize.height / 2, ), ); } } @override bool shouldRelayout(CircleSliceLayoutDelegate oldDelegate) { return angle != oldDelegate.angle; } } ``` Within `performLayout`, we first search for the slice widget by its ID and expand its box constraints to fill the available bounds. The child widget is supposed to only take up space it actually needs, which can be achieved by using loose box constraints. Centering this box within the slice is the tricky part. The image below visualizes the math involved in finding the correct offset for the child. ![https://raw.githubusercontent.com/kevlatus/kevlatus.de/main/public/assets/blog/images/img-circle-slice-layout-logic.png](https://raw.githubusercontent.com/kevlatus/kevlatus.de/main/public/assets/blog/images/img-circle-slice-layout-logic.png) We want to position the child widget on the line, which cuts a given slice in half. To achieve that, we start of with a vector that points to the center of the top edge of our bounding box starting from its top left. This ensures that the vector's length is half the circle's radius. To actually point to the center of our slice, we need to rotate this vector by half the slice's angle, which gives us the blue vector in above illustration.. If you are interested in how the `rotateVector` works, you can find its implementation in [this package's Github repository](https://github.com/kevlatus/flutter_fortune_wheel/blob/main/lib/src/util/util.dart#L10). With the slice's center at hand, the child's offset can be retrieved by adjusting the center to the child's size. If you believe there is a simpler way to achieve the same result, I encourage you to send me a message or comment with your ideas about this problem; I am always glad to learn of new features and tricks. The last aesthetic adjustment is a child widget's rotation. Since I use text children in the examples, it makes sense to run them along the vector's line. As can be seen in the first code sample, this is achieved by wrapping it in a `Transform.rotate` with half the slices angle. But for icons or other use cases no rotation or another angle might be necessary, so this is up to your use case. Finally, we can build a fortune wheel from individual slices, each having correctly centered children. This shows that CustomMultiChildLayouts do not necessarily be composed of many widgets and can also be used for describing complex parent-child relationships. If you have any questions about this example, feel free to reach out to me or consult the [MultiChildLayoutDelegate docs](https://api.flutter.dev/flutter/rendering/MultiChildLayoutDelegate-class.html). In my next article in this series, we will make the wheel actually spin using animation curves. Until then, you can spin the wheel yourself by installing it from [pub.dev](https://pub.dev/packages/flutter_fortune_wheel).

Making-of: Flutter Fortune Wheel

09 January 2021 | ~5 min read
Have you ever been frustrated by not finding a suitable package? Recently I encountered this feeling while looking for a wheel of fortune in Flutter. So I decided to dive deep into Flutter and build one myself. What follows is the story of creating a fortune wheel, like the one shown below, using CustomPainter, LayoutDelegate, animations and more. It is the first part of a bite-sized series on the details of [this package](https://pub.dev/packages/flutter_fortune_wheel). <div align="center"> <img src="https://raw.githubusercontent.com/kevlatus/flutter_fortune_wheel/main/images/img-wheel-256.png"> </div> My main objective was to create an implementation, which is composed of individual Flutter widgets instead of using images, [as other solutions do](https://pub.dev/packages/flutter_spinning_wheel). This allows for making best use of hot reloading and does not require additional tools for creating suitable images. Furthermore, each slice within the wheel should allow for individual styling as well as hosting arbitrary child widgets. Finally, the wheel must spin before showing a selected value to be a real wheel of fortune, which we will achieve using a Bezier-curved animation. Let's start by drawing a simple slice of a pizza...ehm...circle 🍕 ```dart Path buildSlicePath(double radius, double angle) { return Path() ..moveTo(0, 0) ..lineTo(radius, 0) ..arcTo( Rect.fromCircle( center: Offset(0, 0), radius: radius, ), 0, angle, false, ) ..close(); } ``` The code above draws three simple lines: 1. a straight line from the top left corner to the top right corner 2. a curved line from the top right corner to the bottom left corner 3. a straight line from the bottom left corner back to the top left origin Drawing this path using a CustomPainter is shown in the picture below. I added a border to the surrounding widget to highlight that it still has a rectangular shape, as all widgets in Flutter do. In general, this is no issue, but as we will see later, it might complicate positioning, when adding indicators to our wheel. <div align="center"> <img src="https://raw.githubusercontent.com/kevlatus/kevlatus.de/main/public/assets/blog/images/img-circle-slice.png"> </div> ```dart class CircleSlicePainter extends CustomPainter { final double angle; final Color color; const CircleSlicePainter(this.angle, this.color); @override void paint(Canvas canvas, Size size) { final radius = Math.min(size.width, size.height); final path = CircleSlice.buildSlicePath(radius, angle); canvas.drawPath(path, Paint()..color = color); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } } class CircleSliceView extends StatelessWidget { @override Widget build(BuildContext context) { return CustomPaint( painter: CircleSlicePainter(Math.pi / 2, Colors.blue), ); } } ``` This example uses our previously defined `buildSlicePath` function to draw its path on a canvas. The slice's size can be configured using the _angle_ property of the `CircleSlicePainter`, which determines the part of the circle to be drawn, ranging from 0 to 2 × π. Now that we know how to draw a single slice, we can build a circle by drawing many slices and rotating them accordingly. ```dart class CircleView extends StatelessWidget { @override Widget build(BuildContext context) { final colors = <Color>[ Colors.red, Colors.blue, Colors.orange, Colors.indigo, Colors.deepOrange, Colors.green, ]; double anglePerSlice = 2 * Math.pi / colors.length; return Stack( fit: StackFit.expand, children: [ for (int i = 0; i < colors.length; i++) Transform.rotate( angle: i * anglePerSlice, alignment: Alignment.topLeft, child: CustomPaint( painter: CircleSlicePainter( anglePerSlice, colors[i], ), ), ) ], ); } } ``` A slice's size can be computed by dividing its maximum size (2 × π) by the number of slices. The stack widget allows us to put all slices at the same position. Then each slice is rotated according to the angle obtained by multiplying the size of a slice with its index in the circle. The center of our circle is actually in the top left corner of each slice's box, as can be seen by the example of drawing a single slice. Therefore, the alignment for its rotation is set to `Alignment.topLeft`. <div align="center"> <img src="https://raw.githubusercontent.com/kevlatus/kevlatus.de/main/public/assets/blog/images/img-circle.png"> </div> Great! We are now able to draw a circle composed of any number of individually customizable slices. In my next article we will implement a MultiChildLayoutDelegate to correctly position child widgets within the slices. In the meantime, if you don't want to wait for the next article, you can find the package's code [on Github](https://github.com/kevlatus/flutter_fortune_wheel). ### Further reading - [CustomPainter](https://api.flutter.dev/flutter/rendering/CustomPainter-class.html) Flutter docs - [my inspiration](https://github.com/baobao1996mn/flutter-fortune-wheel) for using a path based approach