1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' as math;
6
7import 'basic.dart';
8import 'debug.dart';
9import 'framework.dart';
10
11/// [NavigationToolbar] is a layout helper to position 3 widgets or groups of
12/// widgets along a horizontal axis that's sensible for an application's
13/// navigation bar such as in Material Design and in iOS.
14///
15/// The [leading] and [trailing] widgets occupy the edges of the widget with
16/// reasonable size constraints while the [middle] widget occupies the remaining
17/// space in either a center aligned or start aligned fashion.
18///
19/// Either directly use the themed app bars such as the Material [AppBar] or
20/// the iOS [CupertinoNavigationBar] or wrap this widget with more theming
21/// specifications for your own custom app bar.
22class NavigationToolbar extends StatelessWidget {
23
24 /// Creates a widget that lays out its children in a manner suitable for a
25 /// toolbar.
26 const NavigationToolbar({
27 super.key,
28 this.leading,
29 this.middle,
30 this.trailing,
31 this.centerMiddle = true,
32 this.middleSpacing = kMiddleSpacing,
33 });
34
35 /// The default spacing around the [middle] widget in dp.
36 static const double kMiddleSpacing = 16.0;
37
38 /// Widget to place at the start of the horizontal toolbar.
39 final Widget? leading;
40
41 /// Widget to place in the middle of the horizontal toolbar, occupying
42 /// as much remaining space as possible.
43 final Widget? middle;
44
45 /// Widget to place at the end of the horizontal toolbar.
46 final Widget? trailing;
47
48 /// Whether to align the [middle] widget to the center of this widget or
49 /// next to the [leading] widget when false.
50 final bool centerMiddle;
51
52 /// The spacing around the [middle] widget on horizontal axis.
53 ///
54 /// Defaults to [kMiddleSpacing].
55 final double middleSpacing;
56
57 @override
58 Widget build(BuildContext context) {
59 assert(debugCheckHasDirectionality(context));
60 final TextDirection textDirection = Directionality.of(context);
61 return CustomMultiChildLayout(
62 delegate: _ToolbarLayout(
63 centerMiddle: centerMiddle,
64 middleSpacing: middleSpacing,
65 textDirection: textDirection,
66 ),
67 children: <Widget>[
68 if (leading != null) LayoutId(id: _ToolbarSlot.leading, child: leading!),
69 if (middle != null) LayoutId(id: _ToolbarSlot.middle, child: middle!),
70 if (trailing != null) LayoutId(id: _ToolbarSlot.trailing, child: trailing!),
71 ],
72 );
73 }
74}
75
76enum _ToolbarSlot {
77 leading,
78 middle,
79 trailing,
80}
81
82class _ToolbarLayout extends MultiChildLayoutDelegate {
83 _ToolbarLayout({
84 required this.centerMiddle,
85 required this.middleSpacing,
86 required this.textDirection,
87 });
88
89 // If false the middle widget should be start-justified within the space
90 // between the leading and trailing widgets.
91 // If true the middle widget is centered within the toolbar (not within the horizontal
92 // space between the leading and trailing widgets).
93 final bool centerMiddle;
94
95 /// The spacing around middle widget on horizontal axis.
96 final double middleSpacing;
97
98 final TextDirection textDirection;
99
100 @override
101 void performLayout(Size size) {
102 double leadingWidth = 0.0;
103 double trailingWidth = 0.0;
104
105 if (hasChild(_ToolbarSlot.leading)) {
106 final BoxConstraints constraints = BoxConstraints(
107 maxWidth: size.width,
108 minHeight: size.height, // The height should be exactly the height of the bar.
109 maxHeight: size.height,
110 );
111 leadingWidth = layoutChild(_ToolbarSlot.leading, constraints).width;
112 final double leadingX;
113 switch (textDirection) {
114 case TextDirection.rtl:
115 leadingX = size.width - leadingWidth;
116 case TextDirection.ltr:
117 leadingX = 0.0;
118 }
119 positionChild(_ToolbarSlot.leading, Offset(leadingX, 0.0));
120 }
121
122 if (hasChild(_ToolbarSlot.trailing)) {
123 final BoxConstraints constraints = BoxConstraints.loose(size);
124 final Size trailingSize = layoutChild(_ToolbarSlot.trailing, constraints);
125 final double trailingX;
126 switch (textDirection) {
127 case TextDirection.rtl:
128 trailingX = 0.0;
129 case TextDirection.ltr:
130 trailingX = size.width - trailingSize.width;
131 }
132 final double trailingY = (size.height - trailingSize.height) / 2.0;
133 trailingWidth = trailingSize.width;
134 positionChild(_ToolbarSlot.trailing, Offset(trailingX, trailingY));
135 }
136
137 if (hasChild(_ToolbarSlot.middle)) {
138 final double maxWidth = math.max(size.width - leadingWidth - trailingWidth - middleSpacing * 2.0, 0.0);
139 final BoxConstraints constraints = BoxConstraints.loose(size).copyWith(maxWidth: maxWidth);
140 final Size middleSize = layoutChild(_ToolbarSlot.middle, constraints);
141
142 final double middleStartMargin = leadingWidth + middleSpacing;
143 double middleStart = middleStartMargin;
144 final double middleY = (size.height - middleSize.height) / 2.0;
145 // If the centered middle will not fit between the leading and trailing
146 // widgets, then align its left or right edge with the adjacent boundary.
147 if (centerMiddle) {
148 middleStart = (size.width - middleSize.width) / 2.0;
149 if (middleStart + middleSize.width > size.width - trailingWidth) {
150 middleStart = size.width - trailingWidth - middleSize.width - middleSpacing;
151 } else if (middleStart < middleStartMargin) {
152 middleStart = middleStartMargin;
153 }
154 }
155
156 final double middleX;
157 switch (textDirection) {
158 case TextDirection.rtl:
159 middleX = size.width - middleSize.width - middleStart;
160 case TextDirection.ltr:
161 middleX = middleStart;
162 }
163
164 positionChild(_ToolbarSlot.middle, Offset(middleX, middleY));
165 }
166 }
167
168 @override
169 bool shouldRelayout(_ToolbarLayout oldDelegate) {
170 return oldDelegate.centerMiddle != centerMiddle
171 || oldDelegate.middleSpacing != middleSpacing
172 || oldDelegate.textDirection != textDirection;
173 }
174}
175