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 | |
5 | import 'dart:math' as math; |
6 | |
7 | import 'basic.dart'; |
8 | import 'debug.dart'; |
9 | import '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. |
22 | class 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 | |
76 | enum _ToolbarSlot { |
77 | leading, |
78 | middle, |
79 | trailing, |
80 | } |
81 | |
82 | class _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 | |